fix: use scrollHeight for rendered iframe if offsetHeight is unavailable (#9508)

Fixes #9421.

Added integration test.

Co-authored-by: Gusted <postmaster@gusted.xyz>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9508
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Co-authored-by: Bojidar Marinov <bojidar.marinov.bg@gmail.com>
Co-committed-by: Bojidar Marinov <bojidar.marinov.bg@gmail.com>
This commit is contained in:
Bojidar Marinov
2025-10-16 15:51:57 +02:00
committed by Gusted
co-authored by Gusted
parent e56fdf1ec5
commit 8ed95dc4c6
22 changed files with 125 additions and 6 deletions
-1
View File
@@ -1921,4 +1921,3 @@
is_fsck_enabled: true
close_issues_via_commit_in_any_branch: false
topics: '[]'
+1 -1
View File
@@ -326,7 +326,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() {
// Append a short script to the iframe's contents, which will communicate the scroll height of the embedded document via postMessage, either once loaded (in case the containing page loads first) in response to a postMessage from external.js, in case the iframe loads first
// We use '*' as a target origin for postMessage, because can be certain we are embedded on the same domain, due to X-Frame-Options configured elsewhere. (Plus, the offsetHeight of an embedded document is likely not sensitive data anyway.)
_, _ = pw.Write([]byte("<script>{let postHeight = () => {window.parent.postMessage({frameHeight: document.documentElement.offsetHeight}, '*')}; window.addEventListener('load', postHeight); window.addEventListener('message', (event) => {if (event.source === window.parent && event.data.requestOffsetHeight) postHeight()});}</script>"))
_, _ = pw.Write([]byte("<script>{let postHeight = () => {window.parent.postMessage({frameHeight: document.documentElement.offsetHeight || document.documentElement.scrollHeight}, '*')}; window.addEventListener('load', postHeight); window.addEventListener('message', (event) => {if (event.source === window.parent && event.data.requestOffsetHeight) postHeight()});}</script>"))
}
_ = pw.Close()
+4 -4
View File
@@ -69,7 +69,7 @@ func TestCheckUnadoptedRepositories(t *testing.T) {
func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
username := "user2"
unadoptedList := []string{path.Join(username, "unadopted1"), path.Join(username, "unadopted2")}
unadoptedList := []string{path.Join(username, "rendering-test"), path.Join(username, "unadopted1"), path.Join(username, "unadopted2")}
for _, unadopted := range unadoptedList {
_ = os.Mkdir(path.Join(setting.RepoRootPath, unadopted+".git"), 0o755)
}
@@ -77,13 +77,13 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
opts := db.ListOptions{Page: 1, PageSize: 1}
repoNames, count, err := ListUnadoptedRepositories(db.DefaultContext, "", &opts)
require.NoError(t, err)
assert.Equal(t, 2, count)
assert.Equal(t, 3, count)
assert.Equal(t, unadoptedList[0], repoNames[0])
opts = db.ListOptions{Page: 2, PageSize: 1}
repoNames, count, err = ListUnadoptedRepositories(db.DefaultContext, "", &opts)
require.NoError(t, err)
assert.Equal(t, 2, count)
assert.Equal(t, 3, count)
assert.Equal(t, unadoptedList[1], repoNames[0])
}
@@ -97,7 +97,7 @@ func TestAdoptRepository(t *testing.T) {
path.Join(setting.RepoRootPath, username, unadopted+".git"),
))
opts := db.ListOptions{Page: 1, PageSize: 1}
opts := db.ListOptions{Page: 2, PageSize: 1}
repoNames, _, err := ListUnadoptedRepositories(db.DefaultContext, "", &opts)
require.NoError(t, err)
require.Contains(t, repoNames, path.Join(username, unadopted))
+6
View File
@@ -0,0 +1,6 @@
-
id: 1001
repo_id: 1002
type: 1
config: "{}"
created_unix: 946684810
+15
View File
@@ -11,3 +11,18 @@
status: 0
lfs_size: 8192
topics: '[]'
-
id: 1002
owner_id: 2
owner_name: user2
lower_name: rendering-test
name: rendering-test
default_branch: master
is_empty: false
is_archived: false
is_private: false
status: 0
num_issues: 0
topics: '[]'
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
// @watch start
// web_src/js/markup/external.js
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
test('iframe renderer shrinks to shorter page', async ({page}, _workerInfo) => {
const previewPath = '/user2/rendering-test/src/branch/master/short.iframehtml';
const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
const preview = page.locator('iframe.external-render');
await expect.poll(async () => {
const boundingBox = await preview.boundingBox();
return boundingBox.height;
}).toBeLessThan(300);
});
test('iframe renderer expands to taller page', async ({page}, _workerInfo) => {
const previewPath = '/user2/rendering-test/src/branch/master/tall.iframehtml';
const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
const preview = page.locator('iframe.external-render');
await expect.poll(async () => {
const boundingBox = await preview.boundingBox();
return boundingBox.height;
}).toBeGreaterThan(300);
});
test('iframe renderer expands to taller page with absolutely-positioned body', async ({page}, _workerInfo) => {
const previewPath = '/user2/rendering-test/src/branch/master/absolute.iframehtml';
const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
const preview = page.locator('iframe.external-render');
await expect.poll(async () => {
const boundingBox = await preview.boundingBox();
return boundingBox.height;
}).toBeGreaterThan(300);
});
test('iframe renderer remains at default height if script breaks', async ({page}, _workerInfo) => {
const previewPath = '/user2/rendering-test/src/branch/master/fail.iframehtml';
const response = await page.goto(previewPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
const preview = page.locator('iframe.external-render');
await expect.poll(async () => {
const boundingBox = await preview.boundingBox();
return boundingBox.height;
}).toBeCloseTo(300, 0.5);
});
@@ -0,0 +1 @@
ref: refs/heads/master
@@ -0,0 +1,4 @@
[core]
repositoryformatversion = 0
filemode = true
bare = true
@@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.
@@ -0,0 +1,6 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
@@ -0,0 +1 @@
x+)JMU047g040031QHL*но)-IукL+JлMм(имa╦u╖┼-ЩR╨Эеmгv4ЗЩWqь╪ш╙<-13Y╘iЛ?∙и=ЖlK▌╡В/чo⌠z╟╕╙╢8#©╗Y╜Qж╣*rO?тЬ-s╗тo╧ЕUF╗з▓дc╥_J~!\Бx╖Л{╓÷iо√:иsУсK
@@ -0,0 +1 @@
fafaad77cb54665ac800d1bf77e6a55bd355eabc
+8
View File
@@ -125,3 +125,11 @@ ENABLED = false
[cron.check_repo_stats]
ENABLED = false
# For iframe rendering tests
[markup.iframehtml]
ENABLED = true
FILE_EXTENSIONS = .iframehtml
RENDER_COMMAND = cat
RENDER_CONTENT_MODE = iframe
NEED_POSTPROCESS = false
+8
View File
@@ -139,3 +139,11 @@ ENABLED = false
[cron.check_repo_stats]
ENABLED = false
# For iframe rendering tests
[markup.iframehtml]
ENABLED = true
FILE_EXTENSIONS = .iframehtml
RENDER_COMMAND = cat
RENDER_CONTENT_MODE = iframe
NEED_POSTPROCESS = false
+8
View File
@@ -126,3 +126,11 @@ ENABLED = false
[cron.check_repo_stats]
ENABLED = false
# For iframe rendering tests
[markup.iframehtml]
ENABLED = true
FILE_EXTENSIONS = .iframehtml
RENDER_COMMAND = cat
RENDER_CONTENT_MODE = iframe
NEED_POSTPROCESS = false