Files
makelore/tests/e2e/community-welcome.spec.ts

146 lines
7.8 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[][] };
scope.__communityCalls = [];
Object.defineProperty(window, 'electron', { value: { ipcRenderer: {
invoke: async (...args: unknown[]) => { scope.__communityCalls.push(args); return undefined; },
} } });
});
});
for (const width of [375, 768, 1280, 1470]) {
test(`welcome, composer and community link clear teacher introductions 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);
await expect.poll(async () => {
const a = await heading.boundingBox(), b = await teachers.boundingBox();
return !!a && !!b && (a.x + a.width <= b.x || a.y >= b.y + b.height);
}).toBe(true);
await expect(page.locator('.coding-start-mark')).toHaveCSS('animation-name', 'none');
await page.getByTestId('community-invitation').scrollIntoViewIfNeeded();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
await expect(page.getByRole('button', { name: '去作品广场逛逛' })).toBeVisible();
await teachers.getByRole('button', { name: '先自己试试' }).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);
const invitation = (await page.getByTestId('community-invitation').boundingBox())!;
expect(box.y - invitation.y - invitation.height).toBeCloseTo(12, 0);
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);
});
for (const width of [1280, 1470]) {
test(`the bottom input is centered between the project sidebar and fixed teacher panel 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, teacher, input] = await Promise.all([
page.getByTestId('project-sidebar').boundingBox(), teachers.boundingBox(), composer.boundingBox(),
]);
return Math.abs((input!.x + input!.width / 2) - (side!.x + side!.width + teacher!.x) / 2);
}).toBeLessThan(2);
const input = (await composer.boundingBox())!;
const invitation = (await page.getByTestId('community-invitation').boundingBox())!;
expect(invitation.x).toBeCloseTo(input.x, 0);
expect(invitation.width).toBeCloseTo(input.width, 0);
expect(input.y - invitation.y - invitation.height).toBeCloseTo(12, 0);
await page.screenshot({ path: test.info().outputPath(`composer-between-panels-${width}.png`) });
await composer.getByRole('textbox').fill('保留这份草稿');
await page.getByTestId('project-sidebar').evaluate(el => { el.style.width = '60px'; });
await expect.poll(async () => {
const [side, teacher, input] = await Promise.all([
page.getByTestId('project-sidebar').boundingBox(), teachers.boundingBox(), composer.boundingBox(),
]);
return Math.abs(input!.x + input!.width / 2 - (side!.x + side!.width + teacher!.x) / 2);
}).toBeLessThan(2);
await teachers.getByRole('button', { name: '先自己试试' }).click();
await expect(composer.getByRole('textbox')).toHaveValue('保留这份草稿');
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 community link opens only on click and leaves the draft mounted', 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);
const composer = page.getByRole('textbox');
await composer.fill('我想做一个自己的小镇');
await composer.evaluate(node => node.setAttribute('data-mounted', 'same'));
await page.getByRole('button', { name: '去作品广场逛逛' }).click();
expect(await page.evaluate(() => (window as typeof window & { __communityCalls: unknown[][] }).__communityCalls))
.toContainEqual(['shell:openExternal', 'https://square.nianxx.cn/#explore']);
expect(communityRequests).toEqual([]);
await expect(page.locator('iframe')).toHaveCount(0);
await expect(composer).toHaveValue('我想做一个自己的小镇');
await expect(composer).toHaveAttribute('data-mounted', 'same');
await page.getByRole('button', { name: '发送', exact: true }).click();
await expect(page.getByTestId('community-invitation')).toHaveCount(0);
await expect(composer).toHaveAttribute('data-mounted', 'same');
});