diff --git a/special-pages/pages/new-tab/app/customizer/components/BrowserThemeSection.js b/special-pages/pages/new-tab/app/customizer/components/BrowserThemeSection.js
index 7124cdb426..52b61ec5d6 100644
--- a/special-pages/pages/new-tab/app/customizer/components/BrowserThemeSection.js
+++ b/special-pages/pages/new-tab/app/customizer/components/BrowserThemeSection.js
@@ -9,7 +9,6 @@ import { useComputed } from '@preact/signals';
* @param {(theme: import('../../../types/new-tab').ThemeData) => void} props.setTheme
*/
export function BrowserThemeSection(props) {
- console.log(' RENDER:BrowserThemeSection?');
const current = useComputed(() => props.data.value.theme);
return (
diff --git a/special-pages/pages/new-tab/app/customizer/integration-tests/customizer.page.js b/special-pages/pages/new-tab/app/customizer/integration-tests/customizer.page.js
index 9272d917fa..c8f76f3c89 100644
--- a/special-pages/pages/new-tab/app/customizer/integration-tests/customizer.page.js
+++ b/special-pages/pages/new-tab/app/customizer/integration-tests/customizer.page.js
@@ -1,4 +1,5 @@
-import { expect } from '@playwright/test';
+import { test, expect } from '@playwright/test';
+import { values } from '../values.js';
/**
* @typedef {import('../../../types/new-tab.js').NewTabMessages['subscriptions']['subscriptionEvent']} SubscriptionEventNames
@@ -12,6 +13,95 @@ export class CustomizerPage {
this.ntp = ntp;
}
+ async showsColorSelectionPanel() {
+ const { page } = this.ntp;
+ await page.locator('aside').getByLabel('Solid Colors').click();
+ }
+
+ async opensCustomizer() {
+ const { page } = this.ntp;
+ await page.getByRole('button', { name: 'Customize' }).click();
+ }
+
+ async hasDefaultBackgroundSelected() {
+ const { page } = this.ntp;
+ const selected = page.locator('aside').getByLabel('Default');
+ await expect(selected).toHaveAttribute('aria-checked', 'true');
+ }
+
+ /**
+ * @param {'light' | 'dark'} theme
+ */
+ async mainContentHasTheme(theme) {
+ const { page } = this.ntp;
+ await test.step(`main content area theme should be: ${theme}`, async () => {
+ await expect(page.locator('main')).toHaveAttribute('data-theme', theme);
+ });
+ }
+
+ /**
+ * @param {'light' | 'dark'} theme
+ */
+ async drawerHasTheme(theme) {
+ const { page } = this.ntp;
+ await test.step(`customizer drawer theme should be: ${theme}`, async () => {
+ await expect(page.locator('aside')).toHaveAttribute('data-theme', theme);
+ });
+ }
+
+ async hasColorSelected() {
+ const { page } = this.ntp;
+ const selected = page.locator('aside').getByLabel('Solid Colors');
+ await expect(selected).toHaveAttribute('aria-checked', 'true');
+ }
+
+ async hasGradientSelected() {
+ const { page } = this.ntp;
+ await page.pause();
+ const selected = page.locator('aside').getByLabel('Gradients');
+ await expect(selected).toHaveAttribute('aria-checked', 'true');
+ }
+
+ async hasImagesSelected() {
+ const { page } = this.ntp;
+ const selected = page.locator('aside').getByLabel('My Backgrounds');
+ await expect(selected).toHaveAttribute('aria-checked', 'true');
+ }
+
+ async uploadsFirstImage() {
+ const { page } = this.ntp;
+ await page.getByLabel('Add Background').click();
+ await this.ntp.mocks.waitForCallCount({ count: 1, method: 'customizer_upload' });
+ }
+ async setsDarkTheme() {
+ const { page } = this.ntp;
+ await page.getByRole('radio', { name: 'Select dark theme' }).click();
+ const calls = await this.ntp.mocks.waitForCallCount({ count: 1, method: 'customizer_setTheme' });
+ expect(calls[0].payload).toMatchObject({
+ method: 'customizer_setTheme',
+ params: { theme: 'dark' },
+ });
+ }
+
+ async lightThemeIsSelected() {
+ const { page } = this.ntp;
+ await expect(page.getByRole('radio', { name: 'Select light theme' })).toHaveAttribute('aria-checked', 'true');
+ }
+ async darkThemeIsSelected() {
+ const { page } = this.ntp;
+ await expect(page.getByRole('radio', { name: 'Select dark theme' })).toHaveAttribute('aria-checked', 'true');
+ }
+
+ async selectsDefault() {
+ const { page } = this.ntp;
+ await page.locator('aside').getByLabel('Default').click();
+ const calls = await this.ntp.mocks.waitForCallCount({ count: 1, method: 'customizer_setBackground' });
+ expect(calls[0].payload).toMatchObject({
+ method: 'customizer_setBackground',
+ params: { background: { kind: 'default' } },
+ });
+ }
+
async hasDefaultBackground() {
const { page } = this.ntp;
await expect(page.getByTestId('BackgroundConsumer')).toHaveCSS('background-color', 'rgb(255, 255, 255)');
@@ -21,4 +111,149 @@ export class CustomizerPage {
const { page } = this.ntp;
await expect(page.getByTestId('BackgroundConsumer')).toHaveCSS('background-color', 'rgb(51, 51, 51)');
}
+
+ /**
+ * @param {keyof typeof values.colors} color
+ */
+ async hasColorBackground(color) {
+ const { page } = this.ntp;
+ const value = values.colors[color];
+ await expect(page.getByTestId('BackgroundConsumer')).toHaveAttribute('data-background-color', value.hex);
+ }
+
+ async selectsColor() {
+ const { page } = this.ntp;
+ await this.showsColorSelectionPanel();
+ await page.getByRole('radio', { name: 'Select color03' }).click();
+ const calls = await this.ntp.mocks.waitForCallCount({ count: 1, method: 'customizer_setBackground' });
+ expect(calls[0].payload).toMatchObject({
+ method: 'customizer_setBackground',
+ params: { background: { kind: 'color', value: 'color03' } },
+ });
+ return async () => await page.getByRole('button', { name: 'Solid Colors' }).click();
+ }
+
+ async selectsGradient() {
+ const { page } = this.ntp;
+ await page.locator('aside').getByLabel('Gradients').click();
+ await page.getByRole('radio', { name: 'Select gradient01' }).click();
+ const calls = await this.ntp.mocks.waitForCallCount({ count: 1, method: 'customizer_setBackground' });
+ expect(calls[0].payload).toMatchObject({
+ method: 'customizer_setBackground',
+ params: { background: { kind: 'gradient', value: 'gradient01' } },
+ });
+ return async () => await page.getByRole('button', { name: 'Gradients' }).click();
+ }
+
+ /**
+ * @param {import('../../../types/new-tab.js').BackgroundVariant} bg
+ */
+ async acceptsBackgroundUpdate(bg) {
+ /** @type {import('../../../types/new-tab.js').BackgroundData} */
+ const payload = { background: bg };
+ /** @type {SubscriptionEventNames} */
+ const named = 'customizer_onBackgroundUpdate';
+ await this.ntp.mocks.simulateSubscriptionMessage(named, payload);
+ }
+
+ /**
+ * @param {'light' | 'dark'} theme
+ */
+ async acceptsThemeUpdate(theme) {
+ /** @type {import('../../../types/new-tab.js').ThemeData} */
+ const payload = { theme };
+ /** @type {SubscriptionEventNames} */
+ const named = 'customizer_onThemeUpdate';
+ await this.ntp.mocks.simulateSubscriptionMessage(named, payload);
+ }
+
+ /**
+ * @param {string} color
+ */
+ async acceptsColorUpdate(color) {
+ await test.step('subscription event: customizer_onColorUpdate', async () => {
+ /** @type {import('../../../types/new-tab.js').UserColorData} */
+ const payload = { userColor: { kind: 'hex', value: color } };
+ /** @type {SubscriptionEventNames} */
+ const named = 'customizer_onColorUpdate';
+ await this.ntp.mocks.simulateSubscriptionMessage(named, payload);
+ });
+ }
+
+ /**
+ *
+ */
+ async acceptsImagesUpdate() {
+ const { page } = this.ntp;
+ await test.step('subscription event: customizer_onImagesUpdate', async () => {
+ // Listener for the thumb loading
+ const resPromise = page.waitForResponse((req) => {
+ return req.url().includes(values.userImages['01'].thumb);
+ });
+
+ /** @type {import('../../../types/new-tab.js').UserImageData} */
+ const payload = { userImages: [values.userImages['01']] };
+ /** @type {SubscriptionEventNames} */
+ const named = 'customizer_onImagesUpdate';
+ await this.ntp.mocks.simulateSubscriptionMessage(named, payload);
+
+ const response = await resPromise;
+ await page.pause();
+ expect(response.ok()).toBe(true);
+ });
+ }
+
+ /**
+ * @param {'light' | 'dark'} theme
+ */
+ async hasContentTheme(theme) {
+ const { page } = this.ntp;
+ await expect(page.getByRole('main')).toHaveAttribute('data-theme', theme);
+ }
+
+ /**
+ * @param {string} color
+ */
+ async selectsCustomColor(color) {
+ const { page } = this.ntp;
+ await page.locator('input[type="color"]').click();
+ await page.waitForTimeout(500);
+ await page.locator('input[type="color"]').fill(color);
+ await page.locator('body').click();
+ }
+
+ /**
+ * @param {string} color
+ */
+ async selectsPreviousCustomColor(color) {
+ const { page } = this.ntp;
+ await this.showsColorSelectionPanel();
+ await expect(page.locator('input[type="color"]')).toHaveValue(color);
+ await page.locator('input[type="color"]').click();
+ await page.locator('body').click();
+ }
+
+ /**
+ * @param {string} color
+ */
+ async hasCustomColorValue(color) {
+ const { page } = this.ntp;
+ await expect(page.locator('input[type="color"]'), { message: `input should have value ${color}` }).toHaveValue(color);
+ }
+
+ /**
+ * @param {string} color
+ */
+ async savesTheCustomColor(color) {
+ const calls = await this.ntp.mocks.waitForCallCount({ count: 1, method: 'customizer_setBackground' });
+ expect(calls[0].payload).toMatchObject({
+ method: 'customizer_setBackground',
+ params: { background: { kind: 'hex', value: color } },
+ });
+ }
+
+ async hasEmptyImagesPanel() {
+ const { page } = this.ntp;
+ await page.getByLabel('Add Background').waitFor();
+ }
}
diff --git a/special-pages/pages/new-tab/app/customizer/integration-tests/customizer.spec.js b/special-pages/pages/new-tab/app/customizer/integration-tests/customizer.spec.js
index be8c7360de..ec28f9b0a9 100644
--- a/special-pages/pages/new-tab/app/customizer/integration-tests/customizer.spec.js
+++ b/special-pages/pages/new-tab/app/customizer/integration-tests/customizer.spec.js
@@ -17,6 +17,188 @@ test.describe('newtab customizer', () => {
await ntp.darkMode();
await ntp.openPage({ additional: { customizerDrawer: 'disabled' } });
await cp.hasDefaultDarkBackground();
- await page.pause();
+ });
+ test('loads with the default background', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled' } });
+
+ await cp.opensCustomizer();
+ await cp.hasDefaultBackgroundSelected();
+ });
+ test('respects CSS media query for light/dark when browser theme is "system"', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled', theme: 'system' } });
+ await cp.opensCustomizer();
+
+ // check the main page + drawer both are light theme
+ await cp.mainContentHasTheme('light');
+ await cp.drawerHasTheme('light');
+
+ // emulate changing os-level settings to 'dark'
+ await ntp.darkMode();
+
+ // now assert the themes updated correctly
+ await cp.mainContentHasTheme('dark');
+ await cp.drawerHasTheme('dark');
+ });
+ test('loads with the default background and accepts background update', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled' } });
+
+ await cp.hasDefaultBackground();
+ await cp.opensCustomizer();
+ await cp.hasDefaultBackgroundSelected();
+
+ await cp.acceptsBackgroundUpdate({
+ kind: 'color',
+ value: 'color01',
+ });
+ await cp.hasColorBackground('color01');
+ });
+ test('loads with the default background and accepts theme update', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled' } });
+
+ await cp.opensCustomizer();
+ await cp.hasDefaultBackgroundSelected();
+
+ // this is a control, to ensure it's light before we deliver an update
+ await cp.hasContentTheme('light');
+
+ // now deliver the update and ensure it changed
+ await cp.acceptsThemeUpdate('dark');
+ await cp.hasContentTheme('dark');
+ });
+ test('loads with a color background', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled', background: 'color01', theme: 'dark' } });
+ await cp.opensCustomizer();
+ await cp.hasColorSelected();
+ await cp.acceptsThemeUpdate('light');
+ });
+ test('loads with a color background, and sets back to default', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled', background: 'color01' } });
+ await cp.opensCustomizer();
+ await cp.hasColorSelected();
+ await cp.selectsDefault();
+ });
+ test('loads with default background, and sets a color', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled' } });
+ await cp.opensCustomizer();
+ await cp.hasDefaultBackgroundSelected();
+ const back = await cp.selectsColor();
+ await back();
+ await cp.hasColorSelected();
+ });
+ test('loads with default background, and uses color picker', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled', background: 'default' } });
+ await cp.opensCustomizer();
+ await cp.hasDefaultBackgroundSelected();
+ await cp.showsColorSelectionPanel();
+
+ await cp.selectsCustomColor('#cacaca');
+ await cp.savesTheCustomColor('#cacaca');
+ });
+ test('loads with default background and accepts a color update', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled', background: 'default' } });
+ await cp.opensCustomizer();
+ await cp.hasDefaultBackgroundSelected();
+ await cp.showsColorSelectionPanel();
+
+ await test.step('when a color update is received', async () => {
+ await cp.hasCustomColorValue('#ffffff');
+ await cp.acceptsColorUpdate('#cacaca');
+ });
+
+ await test.step('then the custom color panel should reflect the color', async () => {
+ await cp.hasCustomColorValue('#cacaca');
+ });
+ });
+ test('switches from selected predefined color, to a previously selected hex value', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled', background: 'color01', userColor: 'cacaca' } });
+ await cp.opensCustomizer();
+ await cp.hasColorSelected();
+ await cp.selectsPreviousCustomColor('#cacaca');
+ await cp.savesTheCustomColor('#cacaca');
+ });
+ test('loads with default background, and sets a gradient', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled' } });
+ await cp.opensCustomizer();
+ await cp.hasDefaultBackgroundSelected();
+ const back = await cp.selectsGradient();
+ await back();
+ await cp.hasGradientSelected();
+ });
+ test('loads with a gradient background', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled', background: 'gradient02' } });
+ await cp.opensCustomizer();
+ await cp.hasGradientSelected();
+ });
+ test('loads with a user image', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled', background: 'userImage:01', userImages: 'true' } });
+ await cp.opensCustomizer();
+ await cp.hasImagesSelected();
+ });
+ test('loads without images, and then accepts 1', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled' } });
+ await cp.opensCustomizer();
+ await cp.hasEmptyImagesPanel();
+ await cp.acceptsImagesUpdate();
+ });
+ test('trigger file upload', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled' } });
+ await cp.opensCustomizer();
+ await cp.uploadsFirstImage();
+ });
+ test('Sets theme', async ({ page }, workerInfo) => {
+ const ntp = NewtabPage.create(page, workerInfo);
+ const cp = new CustomizerPage(ntp);
+ await ntp.reducedMotion();
+ await ntp.openPage({ additional: { customizerDrawer: 'enabled', theme: 'light' } });
+ await cp.opensCustomizer();
+ await cp.lightThemeIsSelected();
+ await cp.setsDarkTheme();
+ await cp.darkThemeIsSelected();
});
});