203 lines
11 KiB
TypeScript
203 lines
11 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { createServer, type ViteDevServer } from 'vite';
|
|
import { resolve } from 'node:path';
|
|
|
|
let server: ViteDevServer;
|
|
let baseUrl: string;
|
|
test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' });
|
|
test.beforeAll(async () => {
|
|
server = await createServer({
|
|
configFile: false, cacheDir: 'node_modules/.vite-community-layout',
|
|
resolve: { alias: { '@': resolve('src') } }, esbuild: { jsx: 'automatic' },
|
|
server: { host: '127.0.0.1', port: 0 },
|
|
plugins: [{
|
|
name: 'isolated-community', enforce: 'pre',
|
|
load(id) {
|
|
if (id.endsWith('/src/lib/host-events.ts')) return 'export function subscribeHostEvent() { return () => {}; }';
|
|
if (id.endsWith('/src/lib/host-api.ts')) return `
|
|
export async function createHostEventSource() { return { close() {}, addEventListener() {} }; }
|
|
export async function ensureHostApiToken() { return 'fixture'; }
|
|
export function getHostApiBase() { return ''; }
|
|
export function reportDesktopActivity() {}
|
|
export function setDesktopBackgroundLease() {}
|
|
export async function hostApiFetch() { return {}; }
|
|
`;
|
|
},
|
|
configureServer(vite) {
|
|
vite.middlewares.use((req, res, next) => {
|
|
if (req.url !== '/' && !req.url?.startsWith('/?')) return next();
|
|
res.setHeader('Content-Type', 'text/html');
|
|
res.end('<html><head><meta charset="utf-8"></head><body><div id="root"></div><script type="module" src="/tests/e2e/fixtures/community-welcome.tsx"></script></body></html>');
|
|
});
|
|
},
|
|
}],
|
|
});
|
|
await server.listen();
|
|
baseUrl = server.resolvedUrls!.local[0];
|
|
});
|
|
test.afterAll(async () => { await server?.close(); });
|
|
test.beforeEach(async ({ page }) => {
|
|
page.on('pageerror', error => { throw error; });
|
|
await page.addInitScript(() => {
|
|
const scope = window as typeof window & { __communityCalls: unknown[][]; __communityFailures: number };
|
|
scope.__communityCalls = [];
|
|
scope.__communityFailures = 0;
|
|
Object.defineProperty(window, 'electron', { value: { ipcRenderer: {
|
|
invoke: async (...args: unknown[]) => {
|
|
scope.__communityCalls.push(args);
|
|
if (args[0] === 'shell:openExternal' && scope.__communityFailures > 0) {
|
|
scope.__communityFailures--;
|
|
throw new Error('unavailable');
|
|
}
|
|
return undefined;
|
|
},
|
|
} } });
|
|
});
|
|
});
|
|
|
|
for (const width of [375, 768, 1280, 1470]) {
|
|
test(`welcome and bottom controls remain usable with floating teachers at ${width}px`, async ({ page }) => {
|
|
await page.setViewportSize({ width, height: 850 });
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
await page.goto(baseUrl + '?teachers');
|
|
const heading = page.getByTestId('coding-start-heading');
|
|
const teachers = page.getByTestId('teacher-welcomes');
|
|
await expect(teachers.locator('.teacher-welcome-card')).toHaveCount(2);
|
|
if (width < 900) {
|
|
await expect.poll(async () => {
|
|
const a = await heading.boundingBox(), b = await teachers.boundingBox();
|
|
return !!a && !!b && a.y >= b.y + b.height;
|
|
}).toBe(true);
|
|
}
|
|
await expect(page.locator('.coding-start-mark')).toHaveCSS('animation-name', 'none');
|
|
await page.locator('.coding-start-mark').scrollIntoViewIfNeeded();
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
|
await expect(page.locator('.coding-start-mark')).toBeVisible();
|
|
await expect(page.getByTestId('community-invitation')).toHaveCount(0);
|
|
while (await teachers.count()) await teachers.getByRole('button', { name: /^收起全部老师介绍/ }).first().click();
|
|
await heading.scrollIntoViewIfNeeded();
|
|
const composer = page.getByTestId('coding-message-composer');
|
|
await expect(composer).toBeInViewport({ ratio: 1 });
|
|
const box = (await composer.boundingBox())!;
|
|
expect(850 - box.y - box.height).toBeLessThanOrEqual(40);
|
|
await page.screenshot({ path: test.info().outputPath(`community-welcome-${width}.png`) });
|
|
});
|
|
}
|
|
|
|
test('a short window scrolls the welcome while keeping the composer at the bottom', async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 600 });
|
|
await page.goto(baseUrl + '?teachers');
|
|
const composer = page.getByTestId('coding-message-composer');
|
|
await expect(composer).toBeInViewport({ ratio: 1 });
|
|
const before = (await composer.boundingBox())!;
|
|
await page.locator('.classroom-chat-view').evaluate(el => { el.scrollTop = el.scrollHeight; });
|
|
const after = (await composer.boundingBox())!;
|
|
expect(after.y).toBeCloseTo(before.y, 0);
|
|
expect(600 - after.y - after.height).toBeLessThanOrEqual(40);
|
|
const teachers = page.getByTestId('teacher-welcomes');
|
|
while (await teachers.count()) await teachers.getByRole('button', { name: /^收起全部老师介绍/ }).first().click();
|
|
await page.locator('.coding-start-mark').scrollIntoViewIfNeeded();
|
|
await expect(page.locator('.coding-start-mark')).toBeInViewport({ ratio: 1 });
|
|
await expect(page.getByTestId('community-invitation')).toHaveCount(0);
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375);
|
|
});
|
|
|
|
for (const width of [1280, 1470]) {
|
|
test(`floating teachers leave the welcome and bottom input centered in the page at ${width}px`, async ({ page }) => {
|
|
await page.setViewportSize({ width, height: 850 });
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
await page.goto(baseUrl + '?teachers&sidebar');
|
|
const teachers = page.getByTestId('teacher-welcomes');
|
|
const composer = page.getByTestId('coding-message-composer');
|
|
await expect(teachers).toHaveCSS('width', '340px');
|
|
await expect.poll(async () => {
|
|
const [side, input] = await Promise.all([
|
|
page.getByTestId('project-sidebar').boundingBox(), composer.boundingBox(),
|
|
]);
|
|
return Math.abs(input!.x + input!.width / 2 - (side!.x + side!.width + width) / 2);
|
|
}).toBeLessThan(2);
|
|
const input = (await composer.boundingBox())!;
|
|
await expect(page.getByTestId('community-invitation')).toHaveCount(0);
|
|
const heading = page.getByTestId('coding-start-heading');
|
|
await expect.poll(async () => {
|
|
const [title, content] = await Promise.all([heading.boundingBox(), page.locator('.classroom-chat-view').boundingBox()]);
|
|
return Math.abs(title!.y + title!.height / 2 - content!.y - content!.height / 2);
|
|
}).toBeLessThan(2);
|
|
expect((await heading.boundingBox())!.x + (await heading.boundingBox())!.width / 2).toBeCloseTo(input.x + input.width / 2, 0);
|
|
await page.screenshot({ path: test.info().outputPath(`floating-teachers-centered-welcome-${width}.png`) });
|
|
await composer.getByRole('textbox').fill('保留这份草稿');
|
|
await page.getByTestId('project-sidebar').evaluate(el => { el.style.width = '60px'; });
|
|
await expect.poll(async () => {
|
|
const [side, input] = await Promise.all([
|
|
page.getByTestId('project-sidebar').boundingBox(), composer.boundingBox(),
|
|
]);
|
|
return Math.abs(input!.x + input!.width / 2 - (side!.x + side!.width + width) / 2);
|
|
}).toBeLessThan(2);
|
|
const inputBeforeDismiss = await composer.boundingBox();
|
|
const headingBeforeDismiss = await heading.boundingBox();
|
|
while (await teachers.count()) await teachers.getByRole('button', { name: /^收起全部老师介绍/ }).first().click();
|
|
await expect(composer.getByRole('textbox')).toHaveValue('保留这份草稿');
|
|
expect(await composer.boundingBox()).toEqual(inputBeforeDismiss);
|
|
expect(await heading.boundingBox()).toEqual(headingBeforeDismiss);
|
|
await expect.poll(async () => {
|
|
const [side, input] = await Promise.all([page.getByTestId('project-sidebar').boundingBox(), composer.boundingBox()]);
|
|
return Math.abs(input!.x + input!.width / 2 - (side!.x + side!.width + width) / 2);
|
|
}).toBeLessThan(2);
|
|
});
|
|
}
|
|
|
|
test('the home gallery opens only on click and is larger than the surrounding text', async ({ page }) => {
|
|
await page.setViewportSize({ width: 1280, height: 850 });
|
|
const communityRequests: string[] = [];
|
|
page.on('request', request => { if (request.url().includes('square.nianxx.cn')) communityRequests.push(request.url()); });
|
|
await page.goto(baseUrl + '?home');
|
|
await expect(page.getByText('Alice欢迎回来!', { exact: true })).toBeVisible();
|
|
await expect(page.getByRole('heading', { name: '今天,想创作什么?' })).toBeVisible();
|
|
const entry = page.getByTestId('community-home-link');
|
|
await expect(entry).toHaveText('作品广场');
|
|
await expect(page.locator('.module-selection-inspiration')).toHaveText('还是去「作品广场」找找灵感?');
|
|
const fontRatio = await entry.evaluate(el => parseFloat(getComputedStyle(el).fontSize) / parseFloat(getComputedStyle(el.closest('p')!).fontSize));
|
|
expect(fontRatio).toBeCloseTo(1.12, 2);
|
|
const typography = await entry.evaluate(el => {
|
|
const own = getComputedStyle(el), surrounding = getComputedStyle(el.closest('p')!);
|
|
return { font: own.fontFamily, surroundingFont: surrounding.fontFamily, weight: own.fontWeight, surroundingWeight: surrounding.fontWeight, color: own.color, surroundingColor: surrounding.color };
|
|
});
|
|
expect(typography.font).toBe(typography.surroundingFont);
|
|
expect(typography.weight).toBe(typography.surroundingWeight);
|
|
expect(typography.color).toBe(typography.surroundingColor);
|
|
await expect(entry.locator('svg')).toHaveCount(0);
|
|
expect(await page.evaluate(() => (window as typeof window & { __communityCalls: unknown[][] }).__communityCalls)).toEqual([]);
|
|
await page.screenshot({ path: test.info().outputPath('homepage-gallery-text.png') });
|
|
await entry.click();
|
|
expect(await page.evaluate(() => (window as typeof window & { __communityCalls: unknown[][] }).__communityCalls))
|
|
.toEqual([['shell:openExternal', 'https://square.nianxx.cn/#explore']]);
|
|
expect(communityRequests).toEqual([]);
|
|
await expect(page.locator('iframe')).toHaveCount(0);
|
|
await expect(page.getByRole('heading', { name: '今天,想创作什么?' })).toBeVisible();
|
|
});
|
|
|
|
test('the home text entry supports keyboard retry without overflow on small screens', async ({ page }) => {
|
|
await page.setViewportSize({ width: 375, height: 600 });
|
|
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
await page.goto(baseUrl + '?home');
|
|
await page.evaluate(() => { (window as typeof window & { __communityFailures: number }).__communityFailures = 1; });
|
|
const entry = page.getByTestId('community-home-link');
|
|
await entry.focus();
|
|
await expect(entry).toBeFocused();
|
|
await expect(entry).toHaveCSS('outline-style', 'solid');
|
|
await expect(entry).toHaveCSS('transition-duration', '0s');
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(375);
|
|
await entry.press('Enter');
|
|
await expect(page.getByRole('alert')).toHaveText('没能打开作品广场,请再点一次试试。');
|
|
await expect(entry).toBeEnabled();
|
|
await entry.press('Space');
|
|
await expect(page.getByRole('alert')).toHaveCount(0);
|
|
expect(await page.evaluate(() => (window as typeof window & { __communityCalls: unknown[][] }).__communityCalls))
|
|
.toEqual([
|
|
['shell:openExternal', 'https://square.nianxx.cn/#explore'],
|
|
['shell:openExternal', 'https://square.nianxx.cn/#explore'],
|
|
]);
|
|
await expect(page.getByRole('heading', { name: '今天,想创作什么?' })).toBeVisible();
|
|
await page.screenshot({ path: test.info().outputPath('homepage-gallery-text-mobile.png') });
|
|
});
|