Skip to content
Draft
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
80 changes: 80 additions & 0 deletions special-pages/pages/history/integration-tests/history.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,5 +319,85 @@ test.describe('history', () => {
const calls = await hp.mocks.outgoing({ names: ['reportInitException'] });
expect(calls).toHaveLength(0);
});

test('falls back to ErrorEvent.message when event.error is null', async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(0);
await hp.openPage();
await hp.hasEmptyState();

await page.evaluate(() => {
window.dispatchEvent(new ErrorEvent('error', { message: 'Script error.', error: null }));
});

const calls = await hp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
params: { message: '[uncaught] Script error.' },
},
},
]);
});

test("falls back to 'unknown error' when ErrorEvent has neither error nor message", async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(0);
await hp.openPage();
await hp.hasEmptyState();

await page.evaluate(() => {
window.dispatchEvent(new ErrorEvent('error', { error: null }));
});

const calls = await hp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
params: { message: '[uncaught] unknown error' },
},
},
]);
});

test('stringifies a thrown non-Error value', async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(0);
await hp.openPage();
await hp.hasEmptyState();

await page.evaluate(() => {
setTimeout(() => {
// eslint-disable-next-line no-throw-literal -- intentional: a non-Error throw surfaces via event.error
throw 'raw string';
}, 0);
});

const calls = await hp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
params: { message: '[uncaught] raw string' },
},
},
]);
});

test('reports primitive and nullish rejection reasons distinctly', async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(0);
await hp.openPage();
await hp.hasEmptyState();

await page.evaluate(() => {
/* eslint-disable prefer-promise-reject-errors -- intentional: testing non-Error rejection reasons */
setTimeout(() => Promise.reject(undefined), 0);
setTimeout(() => Promise.reject(null), 0);
setTimeout(() => Promise.reject(0), 0);
/* eslint-enable prefer-promise-reject-errors */
});

const calls = await hp.mocks.waitForCallCount({ method: 'reportInitException', count: 3 });
const messages = calls.map((c) => c.payload.params.message);
expect(messages).toEqual(
expect.arrayContaining(['[unhandledrejection] undefined', '[unhandledrejection] null', '[unhandledrejection] 0']),
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,27 @@ test.describe('omnibar file attachment', () => {
await expect(omnibar.context().getByText('You can only attach 2 files at a time.')).toBeVisible();
});

test('a partial attachmentLimits payload falls back to default file cap without crashing', async ({ page }, workerInfo) => {
const { ntp, omnibar } = setup(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage({
additional: {
'omnibar.mode': 'ai',
'omnibar.enableAiChatTools': 'true',
'omnibar.selectedModelId': 'claude-haiku-4-5',
// Simulates a malformed backend payload: present attachmentLimits but empty sections.
'omnibar.attachmentLimitsPartial': 'empty-sections',
},
});
await omnibar.ready();

await omnibar
.fileInput()
.setInputFiles(['a.pdf', 'b.pdf', 'c.pdf', 'd.pdf'].map((name) => ({ name, mimeType: 'application/pdf', buffer: PDF_BYTES })));
await expect(omnibar.fileChip()).toHaveCount(4);
await expect(omnibar.context().getByText('You can only attach 3 files at a time.')).toBeVisible();
});

test('a file larger than the configured maxFileSizeMB is rejected with an error', async ({ page }, workerInfo) => {
const { ntp, omnibar } = setup(page, workerInfo);
await ntp.reducedMotion();
Expand Down Expand Up @@ -434,6 +455,60 @@ test.describe('omnibar file attachment', () => {
});
});

test.describe('omnibar image attachment limits', () => {
test('the image cap follows the backend attachmentLimits config', async ({ page }, workerInfo) => {
const { ntp, omnibar } = setup(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage({
additional: {
'omnibar.mode': 'ai',
'omnibar.enableAiChatTools': 'true',
'omnibar.selectedModelId': 'gpt-4o-mini',
// Backend-configured cap of two images per turn (default is three).
'omnibar.imageMaxPerTurn': '2',
},
});
await omnibar.ready();

// Two images are within the configured cap — no warning.
await omnibar.fileInput().setInputFiles([
{ name: 'a.png', mimeType: 'image/png', buffer: TINY_PNG },
{ name: 'b.png', mimeType: 'image/png', buffer: TINY_PNG },
]);
await expect(omnibar.imagePreviews()).toHaveCount(2);
await expect(omnibar.imageLimitWarning()).toHaveCount(0);

// A third image exceeds the configured cap of two → warning names the configured limit.
await omnibar.fileInput().setInputFiles({ name: 'c.png', mimeType: 'image/png', buffer: TINY_PNG });
await expect(omnibar.imagePreviews()).toHaveCount(3);
await expect(omnibar.context().getByText('You can only attach 2 images at a time.')).toBeVisible();
await expect(omnibar.chatSubmitButton()).toBeDisabled();
});

test('a partial attachmentLimits payload falls back to default image cap without crashing', async ({ page }, workerInfo) => {
const { ntp, omnibar } = setup(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage({
additional: {
'omnibar.mode': 'ai',
'omnibar.enableAiChatTools': 'true',
'omnibar.selectedModelId': 'gpt-4o-mini',
'omnibar.attachmentLimitsPartial': 'empty-sections',
},
});
await omnibar.ready();

await omnibar.fileInput().setInputFiles([
{ name: 'a.png', mimeType: 'image/png', buffer: TINY_PNG },
{ name: 'b.png', mimeType: 'image/png', buffer: TINY_PNG },
{ name: 'c.png', mimeType: 'image/png', buffer: TINY_PNG },
{ name: 'd.png', mimeType: 'image/png', buffer: TINY_PNG },
]);
await expect(omnibar.imagePreviews()).toHaveCount(4);
await expect(omnibar.context().getByText('You can only attach 3 images at a time.')).toBeVisible();
});
});

test.describe('omnibar attachment coexistence', () => {
test('files and tab content are carried independently on the same submit', async ({ page }, workerInfo) => {
const { ntp, omnibar } = setup(page, workerInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,29 @@
config.enableAskAiSuggestion = parseBooleanQueryParam('omnibar.enableAskAiSuggestion') ?? config.enableAskAiSuggestion;
config.enableAttachTabs = parseBooleanQueryParam('omnibar.enableAttachTabs') ?? config.enableAttachTabs;
if (config.attachmentLimits) {
const attachmentLimitsPartial = url.searchParams.get('omnibar.attachmentLimitsPartial');
if (attachmentLimitsPartial === 'empty-sections') {
config.attachmentLimits = { files: {}, images: {} };

Check failure on line 252 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

Type '{}' is missing the following properties from type '{ maxPerTurn: number; maxPerConversation: number; maxInputCharsWithAttachments: number; }': maxPerTurn, maxPerConversation, maxInputCharsWithAttachments

Check failure on line 252 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

Type '{}' is missing the following properties from type '{ maxPerConversation: number; maxFileSizeMB: number; maxTotalFileSizeBytes: number; maxPagesPerFile: number; }': maxPerConversation, maxFileSizeMB, maxTotalFileSizeBytes, maxPagesPerFile
} else if (attachmentLimitsPartial === 'files-only') {
config.attachmentLimits = { files: {} };

Check failure on line 254 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

Type '{}' is missing the following properties from type '{ maxPerConversation: number; maxFileSizeMB: number; maxTotalFileSizeBytes: number; maxPagesPerFile: number; }': maxPerConversation, maxFileSizeMB, maxTotalFileSizeBytes, maxPagesPerFile
} else if (attachmentLimitsPartial === 'images-only') {
config.attachmentLimits = { images: {} };

Check failure on line 256 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

Type '{}' is missing the following properties from type '{ maxPerTurn: number; maxPerConversation: number; maxInputCharsWithAttachments: number; }': maxPerTurn, maxPerConversation, maxInputCharsWithAttachments
}
const imageMaxPerTurn = parseInt(url.searchParams.get('omnibar.imageMaxPerTurn') ?? '', 10);
if (imageMaxPerTurn > 0) config.attachmentLimits.images.maxPerTurn = imageMaxPerTurn;
if (imageMaxPerTurn > 0) {
config.attachmentLimits.images ??= {};

Check failure on line 260 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

Type '{}' is missing the following properties from type '{ maxPerTurn: number; maxPerConversation: number; maxInputCharsWithAttachments: number; }': maxPerTurn, maxPerConversation, maxInputCharsWithAttachments

Check failure on line 260 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

'config.attachmentLimits' is possibly 'undefined'.
config.attachmentLimits.images.maxPerTurn = imageMaxPerTurn;

Check failure on line 261 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

'config.attachmentLimits' is possibly 'undefined'.
}
const fileMaxPerConversation = parseInt(url.searchParams.get('omnibar.fileMaxPerConversation') ?? '', 10);
if (fileMaxPerConversation > 0) config.attachmentLimits.files.maxPerConversation = fileMaxPerConversation;
if (fileMaxPerConversation > 0) {
config.attachmentLimits.files ??= {};

Check failure on line 265 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

Type '{}' is missing the following properties from type '{ maxPerConversation: number; maxFileSizeMB: number; maxTotalFileSizeBytes: number; maxPagesPerFile: number; }': maxPerConversation, maxFileSizeMB, maxTotalFileSizeBytes, maxPagesPerFile

Check failure on line 265 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

'config.attachmentLimits' is possibly 'undefined'.
config.attachmentLimits.files.maxPerConversation = fileMaxPerConversation;

Check failure on line 266 in special-pages/pages/new-tab/app/omnibar/mocks/omnibar.mock-transport.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-latest)

'config.attachmentLimits' is possibly 'undefined'.
}
const fileMaxFileSizeMB = parseInt(url.searchParams.get('omnibar.fileMaxFileSizeMB') ?? '', 10);
if (fileMaxFileSizeMB > 0) config.attachmentLimits.files.maxFileSizeMB = fileMaxFileSizeMB;
if (fileMaxFileSizeMB > 0) {
config.attachmentLimits.files ??= {};
config.attachmentLimits.files.maxFileSizeMB = fileMaxFileSizeMB;
}
}
return config;
}
Expand Down
84 changes: 84 additions & 0 deletions special-pages/pages/new-tab/integration-tests/new-tab.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,5 +214,89 @@ test.describe('newtab widgets', () => {
const calls = await ntp.mocks.outgoing({ names: ['reportInitException'] });
expect(calls).toHaveLength(0);
});

test('falls back to ErrorEvent.message when event.error is null', async ({ page }, workerInfo) => {
const ntp = NewtabPage.create(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage();
await ntp.waitForCustomizer();

await page.evaluate(() => {
window.dispatchEvent(new ErrorEvent('error', { message: 'Script error.', error: null }));
});

const calls = await ntp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
params: { message: '[uncaught] Script error.' },
},
},
]);
});

test("falls back to 'unknown error' when ErrorEvent has neither error nor message", async ({ page }, workerInfo) => {
const ntp = NewtabPage.create(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage();
await ntp.waitForCustomizer();

await page.evaluate(() => {
window.dispatchEvent(new ErrorEvent('error', { error: null }));
});

const calls = await ntp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
params: { message: '[uncaught] unknown error' },
},
},
]);
});

test('stringifies a thrown non-Error value', async ({ page }, workerInfo) => {
const ntp = NewtabPage.create(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage();
await ntp.waitForCustomizer();

await page.evaluate(() => {
setTimeout(() => {
// eslint-disable-next-line no-throw-literal -- intentional: a non-Error throw surfaces via event.error
throw 'raw string';
}, 0);
});

const calls = await ntp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
params: { message: '[uncaught] raw string' },
},
},
]);
});

test('reports primitive and nullish rejection reasons distinctly', async ({ page }, workerInfo) => {
const ntp = NewtabPage.create(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage();
await ntp.waitForCustomizer();

await page.evaluate(() => {
/* eslint-disable prefer-promise-reject-errors -- intentional: testing non-Error rejection reasons */
setTimeout(() => Promise.reject(undefined), 0);
setTimeout(() => Promise.reject(null), 0);
setTimeout(() => Promise.reject(0), 0);
/* eslint-enable prefer-promise-reject-errors */
});

const calls = await ntp.mocks.waitForCallCount({ method: 'reportInitException', count: 3 });
const messages = calls.map((c) => c.payload.params.message);
expect(messages).toEqual(
expect.arrayContaining(['[unhandledrejection] undefined', '[unhandledrejection] null', '[unhandledrejection] 0']),
);
});
});
});
Loading