mirror of
https://github.com/apache/superset.git
synced 2026-09-01 13:01:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d5b7b8399 | ||
|
|
8d392d8ebb | ||
|
|
5334b9ae3b | ||
|
|
442b3fb0f7 |
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import { render, unmountComponentAtNode } from 'react-dom';
|
||||||
|
import { act } from '@testing-library/react';
|
||||||
|
import { ThemeProvider } from '@emotion/react';
|
||||||
|
import { Theme } from './Theme';
|
||||||
|
import { GlobalStyles } from './GlobalStyles';
|
||||||
|
import { ThemeAlgorithm } from './types';
|
||||||
|
import * as themeUtils from './utils/themeUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read all CSS text injected into the document by Emotion's Global component.
|
||||||
|
* Emotion uses insertRule() so cssRules is authoritative; textContent is empty.
|
||||||
|
*/
|
||||||
|
function getInjectedCss(): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const sheet of Array.from(document.styleSheets)) {
|
||||||
|
try {
|
||||||
|
for (const rule of Array.from(sheet.cssRules)) {
|
||||||
|
parts.push(rule.cssText);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Cross-origin sheets are inaccessible — skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
container = document.createElement('div');
|
||||||
|
document.body.appendChild(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
container.remove();
|
||||||
|
// Remove any style tags injected by Emotion to keep tests isolated
|
||||||
|
document.querySelectorAll('style[data-emotion]').forEach(el => el.remove());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GlobalStyles injects color-scheme: light for a light theme', () => {
|
||||||
|
const lightTheme = Theme.fromConfig({});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme.theme}>
|
||||||
|
<GlobalStyles />
|
||||||
|
</ThemeProvider>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getInjectedCss()).toMatch(/color-scheme\s*:\s*light/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GlobalStyles injects color-scheme: dark for a dark theme', () => {
|
||||||
|
const darkTheme = Theme.fromConfig({ algorithm: ThemeAlgorithm.DARK });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider theme={darkTheme.theme}>
|
||||||
|
<GlobalStyles />
|
||||||
|
</ThemeProvider>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getInjectedCss()).toMatch(/color-scheme\s*:\s*dark/);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Embedded context simulation:
|
||||||
|
* Embedded dashboards initialize with ThemeMode.DEFAULT (light). Without
|
||||||
|
* an explicit color-scheme declaration, OS dark mode leaks into the iframe
|
||||||
|
* and applies dark borders/scrollbars. This test verifies the injected CSS
|
||||||
|
* declares color-scheme: light (preventing the OS preference from winning)
|
||||||
|
* and explicitly does NOT declare color-scheme: dark.
|
||||||
|
*/
|
||||||
|
test('GlobalStyles prevents OS dark mode from affecting embedded light-mode dashboards', () => {
|
||||||
|
// EmbeddedContextProviders always initializes with ThemeMode.DEFAULT (light theme)
|
||||||
|
const embeddedLightTheme = Theme.fromConfig({});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider theme={embeddedLightTheme.theme}>
|
||||||
|
<GlobalStyles />
|
||||||
|
</ThemeProvider>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const css = getInjectedCss();
|
||||||
|
// Must declare light to override any prefers-color-scheme: dark from OS
|
||||||
|
expect(css).toMatch(/color-scheme\s*:\s*light/);
|
||||||
|
// Must NOT declare dark — that would reproduce the original bug
|
||||||
|
expect(css).not.toMatch(/color-scheme\s*:\s*dark/);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Theme switching test:
|
||||||
|
* Confirms that when the active theme changes from light to dark (e.g., via
|
||||||
|
* the Switchboard setThemeMode API), the color-scheme declaration updates
|
||||||
|
* accordingly so native browser UI stays in sync with the app theme.
|
||||||
|
*/
|
||||||
|
test('GlobalStyles updates color-scheme dynamically when theme switches light → dark', () => {
|
||||||
|
const lightTheme = Theme.fromConfig({});
|
||||||
|
const darkTheme = Theme.fromConfig({ algorithm: ThemeAlgorithm.DARK });
|
||||||
|
|
||||||
|
// Initial render: light theme → color-scheme: light
|
||||||
|
act(() => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme.theme}>
|
||||||
|
<GlobalStyles />
|
||||||
|
</ThemeProvider>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getInjectedCss()).toMatch(/color-scheme\s*:\s*light/);
|
||||||
|
|
||||||
|
// Clean up Emotion styles before re-rendering with dark theme
|
||||||
|
document.querySelectorAll('style[data-emotion]').forEach(el => el.remove());
|
||||||
|
|
||||||
|
// Re-render with dark theme → color-scheme: dark
|
||||||
|
act(() => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider theme={darkTheme.theme}>
|
||||||
|
<GlobalStyles />
|
||||||
|
</ThemeProvider>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getInjectedCss()).toMatch(/color-scheme\s*:\s*dark/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GlobalStyles calls isThemeDark with the current theme to determine color-scheme', () => {
|
||||||
|
const isThemeDarkSpy = jest.spyOn(themeUtils, 'isThemeDark');
|
||||||
|
const lightTheme = Theme.fromConfig({});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme.theme}>
|
||||||
|
<GlobalStyles />
|
||||||
|
</ThemeProvider>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isThemeDarkSpy).toHaveBeenCalledWith(lightTheme.theme);
|
||||||
|
expect(isThemeDarkSpy).toHaveReturnedWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GlobalStyles detects dark theme via isThemeDark', () => {
|
||||||
|
const isThemeDarkSpy = jest.spyOn(themeUtils, 'isThemeDark');
|
||||||
|
const darkTheme = Theme.fromConfig({ algorithm: ThemeAlgorithm.DARK });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider theme={darkTheme.theme}>
|
||||||
|
<GlobalStyles />
|
||||||
|
</ThemeProvider>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isThemeDarkSpy).toHaveBeenCalledWith(darkTheme.theme);
|
||||||
|
expect(isThemeDarkSpy).toHaveReturnedWith(true);
|
||||||
|
});
|
||||||
@@ -29,13 +29,24 @@ import '@fontsource/ibm-plex-mono/600.css';
|
|||||||
/* eslint-enable import/extensions */
|
/* eslint-enable import/extensions */
|
||||||
|
|
||||||
import { css, useTheme, Global } from '@emotion/react';
|
import { css, useTheme, Global } from '@emotion/react';
|
||||||
|
import type { SupersetTheme } from './types';
|
||||||
|
import { isThemeDark } from './utils/themeUtils';
|
||||||
|
|
||||||
export const GlobalStyles = () => {
|
export const GlobalStyles = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme() as SupersetTheme;
|
||||||
|
const isDark = isThemeDark(theme);
|
||||||
return (
|
return (
|
||||||
<Global
|
<Global
|
||||||
key={`global-${theme.colorLink}`}
|
key={`global-${theme.colorLink}`}
|
||||||
styles={css`
|
styles={css`
|
||||||
|
/* Explicitly declare color-scheme so the browser uses matching native
|
||||||
|
styling (scrollbars, form controls, etc.) rather than inheriting the
|
||||||
|
OS dark-mode preference. Without this, an embedded iframe renders
|
||||||
|
dark borders/scrollbars even when Superset's theme is light. */
|
||||||
|
html {
|
||||||
|
color-scheme: ${isDark ? 'dark' : 'light'};
|
||||||
|
}
|
||||||
|
|
||||||
// SPA
|
// SPA
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Experimental E2E tests for sc-100647:
|
||||||
|
* "Dark border in embedded dashboards when OS is in Dark Mode"
|
||||||
|
*
|
||||||
|
* Bug: When a user's OS is set to dark mode (e.g. macOS), embedded Superset
|
||||||
|
* dashboards show unexpected dark borders/lines because the browser's UA
|
||||||
|
* stylesheet applies dark native styling (scrollbars, form controls, etc.)
|
||||||
|
* to the iframe. The fix sets `color-scheme: light` on the `html` element
|
||||||
|
* via GlobalStyles so native browser chrome matches the app's light theme.
|
||||||
|
*
|
||||||
|
* These tests emulate OS dark mode via Playwright's `colorScheme: 'dark'`
|
||||||
|
* option, then verify the `html` element's `color-scheme` CSS property is
|
||||||
|
* explicitly declared so native styling does not leak in.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect, Browser } from '@playwright/test';
|
||||||
|
import { TIMEOUT } from '../../utils/constants';
|
||||||
|
|
||||||
|
const ADMIN_USERNAME = process.env.PLAYWRIGHT_ADMIN_USERNAME || 'admin';
|
||||||
|
const ADMIN_PASSWORD = process.env.PLAYWRIGHT_ADMIN_PASSWORD || 'general';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate the resolved color-scheme on the html element.
|
||||||
|
* Uses getComputedStyle so both inline and stylesheet declarations are captured.
|
||||||
|
*/
|
||||||
|
async function getHtmlColorScheme(
|
||||||
|
page: import('@playwright/test').Page,
|
||||||
|
): Promise<string> {
|
||||||
|
return page.evaluate(() =>
|
||||||
|
getComputedStyle(document.documentElement).colorScheme.trim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: create a new browser context emulating OS dark mode with saved admin auth.
|
||||||
|
*/
|
||||||
|
async function darkModeAdminContext(browser: Browser) {
|
||||||
|
return browser.newContext({
|
||||||
|
colorScheme: 'dark',
|
||||||
|
storageState: 'playwright/.auth/user.json',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test('html element has color-scheme: light on the welcome page when OS is in dark mode', async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
const context = await darkModeAdminContext(browser);
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto('superset/welcome/', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
const colorScheme = await getHtmlColorScheme(page);
|
||||||
|
|
||||||
|
// Superset's default theme is light. Even with OS dark mode emulated,
|
||||||
|
// GlobalStyles must declare color-scheme: light so native browser elements
|
||||||
|
// (scrollbars, form controls, etc.) render in light mode inside the iframe.
|
||||||
|
expect(colorScheme).toBe('light');
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('html element has color-scheme: light on a dashboard page when OS is in dark mode', async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
// Create an admin context emulating OS dark mode
|
||||||
|
const context = await darkModeAdminContext(browser);
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
// Admin context for API calls (uses saved auth, not dark-mode context)
|
||||||
|
const adminContext = await browser.newContext({
|
||||||
|
storageState: 'playwright/.auth/user.json',
|
||||||
|
});
|
||||||
|
const adminPage = await adminContext.newPage();
|
||||||
|
|
||||||
|
let dashboardId: number | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a minimal published dashboard via API
|
||||||
|
const createRes = await adminPage.request.post('api/v1/dashboard/', {
|
||||||
|
data: {
|
||||||
|
dashboard_title: `e2e_dark_mode_test_${Date.now()}`,
|
||||||
|
published: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(createRes.ok()).toBe(true);
|
||||||
|
const body = await createRes.json();
|
||||||
|
dashboardId = body.id;
|
||||||
|
expect(dashboardId).toBeTruthy();
|
||||||
|
|
||||||
|
// Navigate to the dashboard with OS dark mode emulated
|
||||||
|
await page.goto(`superset/dashboard/${dashboardId}/`, {
|
||||||
|
waitUntil: 'networkidle',
|
||||||
|
timeout: TIMEOUT.PAGE_LOAD,
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorScheme = await getHtmlColorScheme(page);
|
||||||
|
|
||||||
|
// color-scheme: light must be present so the embedded iframe's native
|
||||||
|
// browser UI (borders, scrollbars, inputs) stays in light mode.
|
||||||
|
expect(colorScheme).toBe('light');
|
||||||
|
} finally {
|
||||||
|
// Cleanup
|
||||||
|
if (dashboardId) {
|
||||||
|
await adminPage.request
|
||||||
|
.delete(`api/v1/dashboard/${dashboardId}`)
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
await adminContext.close();
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('html element has color-scheme: light on the embedded dashboard route when OS is in dark mode', async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
|
||||||
|
// This test exercises the actual embedded route (/embedded/<uuid>/ or
|
||||||
|
// /dashboard/<id>/embedded/) which is the code path affected by sc-100647.
|
||||||
|
// Because the embedded route requires a guest token (set up via Switchboard),
|
||||||
|
// we test the next best thing: the /dashboard/<id>/embedded/ URL which loads
|
||||||
|
// the same EmbeddedContextProviders → GlobalStyles stack without a guest token
|
||||||
|
// check, allowing us to verify the color-scheme declaration is present.
|
||||||
|
|
||||||
|
const adminContext = await browser.newContext({
|
||||||
|
storageState: 'playwright/.auth/user.json',
|
||||||
|
});
|
||||||
|
const adminPage = await adminContext.newPage();
|
||||||
|
|
||||||
|
const darkContext = await browser.newContext({
|
||||||
|
colorScheme: 'dark',
|
||||||
|
storageState: 'playwright/.auth/user.json',
|
||||||
|
});
|
||||||
|
const page = await darkContext.newPage();
|
||||||
|
|
||||||
|
let dashboardId: number | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createRes = await adminPage.request.post('api/v1/dashboard/', {
|
||||||
|
data: {
|
||||||
|
dashboard_title: `e2e_embedded_dark_mode_${Date.now()}`,
|
||||||
|
published: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(createRes.ok()).toBe(true);
|
||||||
|
const body = await createRes.json();
|
||||||
|
dashboardId = body.id;
|
||||||
|
|
||||||
|
// The /embedded/ sub-route renders EmbeddedContextProviders which initializes
|
||||||
|
// ThemeMode.DEFAULT (light). GlobalStyles must inject color-scheme: light.
|
||||||
|
await page.goto(`dashboard/${dashboardId}/embedded/`, {
|
||||||
|
waitUntil: 'load',
|
||||||
|
timeout: TIMEOUT.PAGE_LOAD,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for React to hydrate and Emotion to inject global styles
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const sheets = Array.from(document.styleSheets);
|
||||||
|
return sheets.some(sheet => {
|
||||||
|
try {
|
||||||
|
return Array.from(sheet.cssRules).some(rule =>
|
||||||
|
rule.cssText.includes('color-scheme'),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ timeout: TIMEOUT.PAGE_LOAD },
|
||||||
|
);
|
||||||
|
|
||||||
|
const colorScheme = await getHtmlColorScheme(page);
|
||||||
|
expect(colorScheme).toBe('light');
|
||||||
|
} finally {
|
||||||
|
if (dashboardId) {
|
||||||
|
await adminPage.request
|
||||||
|
.delete(`api/v1/dashboard/${dashboardId}`)
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
await adminContext.close();
|
||||||
|
await darkContext.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('html element has color-scheme: dark when Superset dark theme is active with OS dark mode', async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
// Sanity-check the inverse: when the user explicitly switches to dark theme,
|
||||||
|
// color-scheme should be dark (not stuck on light).
|
||||||
|
// This confirms dynamic switching works end-to-end.
|
||||||
|
//
|
||||||
|
// Note: This test navigates to the welcome page and toggles dark mode via
|
||||||
|
// localStorage (the ThemeController persists mode there for non-embedded pages).
|
||||||
|
|
||||||
|
const context = await browser.newContext({
|
||||||
|
colorScheme: 'dark',
|
||||||
|
storageState: 'playwright/.auth/user.json',
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Pre-set dark mode in localStorage before loading the page
|
||||||
|
await context.addInitScript(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('superset_theme_mode', 'dark');
|
||||||
|
} catch {
|
||||||
|
// localStorage may not be available in all contexts
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('superset/welcome/', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
const colorScheme = await getHtmlColorScheme(page);
|
||||||
|
|
||||||
|
// With dark theme active, color-scheme should be dark so native browser
|
||||||
|
// UI (scrollbars, inputs) renders dark to match the app theme.
|
||||||
|
expect(colorScheme).toBe('dark');
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login helper for tests that need a fresh session (not using saved auth).
|
||||||
|
*/
|
||||||
|
async function loginAs(
|
||||||
|
page: import('@playwright/test').Page,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
) {
|
||||||
|
await page.goto('login/');
|
||||||
|
await page.fill('input[name="username"]', username);
|
||||||
|
await page.fill('input[name="password"]', password);
|
||||||
|
await page.click('[type="submit"]');
|
||||||
|
await page.waitForURL('**/superset/welcome/**', {
|
||||||
|
timeout: TIMEOUT.PAGE_LOAD,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('color-scheme: light is present after fresh login with OS dark mode emulated', async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
// Test the full login → welcome flow without pre-saved auth state
|
||||||
|
// to ensure GlobalStyles applies color-scheme from the very first paint.
|
||||||
|
const context = await browser.newContext({ colorScheme: 'dark' });
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loginAs(page, ADMIN_USERNAME, ADMIN_PASSWORD);
|
||||||
|
|
||||||
|
const colorScheme = await getHtmlColorScheme(page);
|
||||||
|
expect(colorScheme).toBe('light');
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user