Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions browser/src/app/ViewLayoutMultiPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ class ViewLayoutMultiPage extends ViewLayoutNewBase {

protected refreshVisibleAreaRectangle(): void {
const documentAnchor = this.getDocumentAnchorSection();

// When the document container is hidden (e.g. BackstageView in CODA), the
// anchor section has zero size - bail out to avoid an infinite retry loop.
if (documentAnchor.size[0] <= 0 || documentAnchor.size[1] <= 0) return;

const view = cool.SimpleRectangle.fromCorePixels([
this.scrollProperties.viewX,
this.scrollProperties.viewY,
Expand Down
Binary file added qt/test/fixtures/scrolling.odt
Binary file not shown.
58 changes: 58 additions & 0 deletions qt/test/lib/file-dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright the Collabora Online contributors.
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { join } from 'path';
import * as webview from './webview.js';

/**
* Open a file from the test fixtures directory via the native Qt file dialog.
*
* Triggers .uno:Open, fills the filename field, clicks Open, switches to
* the new WebView, and waits for the document to finish loading.
*/
export async function openFixture(
webEngine: WebdriverIO.Browser,
native: WebdriverIO.Browser,
fileName: string,
): Promise<void> {
// Use setTimeout so execute() returns before the modal dialog blocks.
await webEngine.execute(() => {
setTimeout(() => {
window.postMobileMessage('uno .uno:Open');
}, 100);
});

const fileNameField = await native.$(
'//*[@accessibility-id="QApplication.QFileDialog.fileNameEdit"]',
);
await fileNameField.waitForExist({ timeout: 10000 });

const filePath = join(process.env.CODA_QT_TEST_DOCUMENTS_DIR!, fileName);
await fileNameField.setValue(filePath);

const openBtn = await native.$(
'//*[@accessibility-id="QApplication.QFileDialog.buttonBox.QPushButton"]',
);
await openBtn.waitForExist({ timeout: 5000 });
await openBtn.click();

await webview.switchToNewWebView(webEngine);

await (webEngine as any).waitForCondition(
() =>
typeof app !== 'undefined' &&
app.map &&
app.map._docLoaded === true,
{
timeout: 45000,
timeoutMsg: `Document did not load after opening ${fileName}`,
},
);
}
98 changes: 98 additions & 0 deletions qt/test/specs/multipage-backstage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright the Collabora Online contributors.
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { openFixture } from '../lib/file-dialog.js';

// Regression test for https://github.com/CollaboraOnline/online/issues/15291
//
// When BackstageView is shown while multi-page view is active, hiding
// #document-container triggers ResizeObservers that shrink the canvas to
// zero. ViewLayoutMultiPage.refreshVisibleAreaRectangle() then enters an
// infinite retry loop (no page rectangles intersect a zero-sized viewport),
// making the UI completely unresponsive.
describe('Multi-page view backstage (issue #15291)', () => {
it('should not freeze when opening backstage in multi-page view', async function () {
await openFixture(browser.webEngine, browser.native, 'scrolling.odt');

// Enable multi-page view.
await browser.webEngine.execute(() => {
app.dispatcher.dispatch('multipageview');
});

// Wait for multi-page view to be active with multiple pages.
await browser.webEngine.waitForCondition(
() => {
const layout = app.activeDocument?.activeLayout;
return !!(
layout?.type === 'ViewLayoutMultiPage' &&
layout.documentRectangles &&
layout.documentRectangles.length > 1
);
},
{
timeout: 15000,
timeoutMsg:
'Multi-page view did not activate with multiple pages',
},
);

// Wait for layout tasks to settle before triggering backstage.
await browser.webEngine.waitForCondition(
() => !app.layoutingService.hasTasksPending(),
{ timeout: 10000, timeoutMsg: 'Layout tasks did not settle' },
);

// Show the backstage view (same as clicking the File tab).
// Without the fix this causes an infinite layout task loop.
await browser.webEngine.execute(() => {
app.map.backstageView!.show();
});

// Verify layout tasks drain. Without the fix, hasTasksPending()
// stays true forever due to the infinite retry loop in
// refreshVisibleAreaRectangle().
await browser.webEngine.waitForCondition(
() => !app.layoutingService.hasTasksPending(),
{
timeout: 5000,
timeoutMsg:
'Layout tasks did not drain - infinite loop detected (issue #15291)',
},
);

// Close the backstage.
await browser.webEngine.execute(() => {
app.map.backstageView!.hide();
});

// Verify multi-page view recovered after backstage is closed.
await browser.webEngine.waitForCondition(
() => {
const container = document.getElementById(
'document-container',
);
if (!container || container.classList.contains('hidden'))
return false;

const layout = app.activeDocument?.activeLayout;
if (!layout || layout.type !== 'ViewLayoutMultiPage')
return false;

const vr = layout.viewedRectangle;
return !!(vr && vr.pWidth > 0 && vr.pHeight > 0);
},
{
timeout: 15000,
timeoutMsg:
'Multi-page view did not recover after backstage',
},
);
});
});
39 changes: 2 additions & 37 deletions qt/test/specs/open-file.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { join } from 'path';
import * as webview from '../lib/webview.js';
import { openFixture } from '../lib/file-dialog.js';

describe('Open existing file', () => {
it('should open a .odt file via the native file dialog', async function () {
// Send the UNO Open command via the bridge. Use setTimeout so
// execute() returns before the modal dialog blocks the WebView.
await browser.webEngine.execute(() => {
setTimeout(() => {
window.postMobileMessage('uno .uno:Open');
}, 100);
});

// Wait for the filename field; accessibility-id is locale-stable.
const fileNameField = await browser.native.$(
'//*[@accessibility-id="QApplication.QFileDialog.fileNameEdit"]',
);
await fileNameField.waitForExist({ timeout: 10000 });

const filePath = join(process.env.CODA_QT_TEST_DOCUMENTS_DIR!, 'simple.odt');
await fileNameField.setValue(filePath);

// First QPushButton is "Open"
const openBtn = await browser.native.$(
'//*[@accessibility-id="QApplication.QFileDialog.buttonBox.QPushButton"]',
);
await openBtn.waitForExist({ timeout: 5000 });
await openBtn.click();

await webview.switchToNewWebView(browser.webEngine);

// Use _docLoaded; opening a file may not auto-focus the cursor.
await browser.webEngine.waitForCondition(
() =>
typeof app !== 'undefined' && app.map && app.map._docLoaded === true,
{
timeout: 45000,
timeoutMsg: 'Document editor did not load after opening file',
},
);
await openFixture(browser.webEngine, browser.native, 'simple.odt');

const state = await browser.webEngine.execute(() => ({
docLoaded: app.map._docLoaded,
Expand Down
18 changes: 18 additions & 0 deletions qt/test/wdio.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,23 @@ declare const app: {
map: {
_docLoaded: boolean;
getDocType(): string;
backstageView?: {
show(): void;
hide(): void;
toggle(): void;
};
};
dispatcher: {
dispatch(action: string): void;
};
activeDocument?: {
activeLayout?: {
type: string;
documentRectangles?: Array<unknown>;
viewedRectangle?: { pWidth: number; pHeight: number };
};
};
layoutingService: {
hasTasksPending(): boolean;
};
};
Loading