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(); }); });