Files
hop.noname.company/tests/e2e/device-menu.mjs

119 lines
6.1 KiB
JavaScript

import assert from 'node:assert/strict';
import { chromium } from '@playwright/test';
import { mkdtemp, rm } from 'node:fs/promises';
import { resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { loadConfig } from '../../apps/server/src/config.ts';
import { createApp } from '../../apps/server/src/app.ts';
const folder = await mkdtemp(resolve(tmpdir(), 'hop-devices-e2e-'));
const runtime = await createApp({
...loadConfig({
APP_URL: 'http://127.0.0.1',
DATABASE_PATH: resolve(folder, 'db.sqlite'),
UPLOAD_DIR: resolve(folder, 'uploads'),
MIN_FREE_DISK_MB: '0',
SESSION_SECRET: 's'.repeat(40),
DISCOVERY_SECRET: 'd'.repeat(40),
}),
webRoot: resolve('apps/web/dist'),
});
let browser;
try {
await runtime.app.listen({ host: '127.0.0.1', port: 0 });
const { port } = runtime.app.server.address(),
url = `http://127.0.0.1:${port}`;
runtime.setDevOrigin(url);
browser = await chromium.launch({
headless: true,
...(process.env.HOP_BROWSER_EXECUTABLE
? { executablePath: process.env.HOP_BROWSER_EXECUTABLE }
: {}),
});
const hostContext = await browser.newContext({ viewport: { width: 1440, height: 1050 } }),
guestContext = await browser.newContext({
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
});
const host = await hostContext.newPage(),
guest = await guestContext.newPage(),
errors = [];
for (const page of [host, guest]) page.on('pageerror', (error) => errors.push(error.message));
await Promise.all([host.goto(url), guest.goto(url)]);
async function rename(page, name) {
await page.locator('.device-name').click();
await page.getByRole('textbox', { name: '기기 이름' }).fill(name);
await page.getByRole('button', { name: '이름 저장' }).click();
await page.locator('.device-name').filter({ hasText: name }).waitFor();
}
await rename(host, '방장 Mac');
await rename(guest, '외부 기기');
await host.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
await host.getByRole('button', { name: '연결 공간 만들기' }).click();
await host.getByRole('checkbox', { name: '참여할 때 내 승인 받기' }).check();
await host.getByRole('button', { name: '만들기', exact: true }).click();
await host.locator('.invite-code').waitFor();
const code = (await host.locator('.invite-code').innerText()).replace(/\s/g, '');
await host.getByRole('button', { name: '대화상자 닫기' }).click();
await host.locator('.my-device-info .host-badge').waitFor();
await guest.getByRole('button', { name: '다른 기기 연결', exact: true }).click();
await guest.getByRole('button', { name: '연결 공간 참여', exact: true }).click();
await guest.getByRole('textbox', { name: '연결 코드' }).fill(code);
await guest.getByRole('button', { name: '참여하기', exact: true }).click();
await guest.locator('.room-leave').waitFor();
await guest.locator('.modal-backdrop').waitFor({ state: 'hidden' });
const guestCard = host.locator('.peer-entry').filter({ hasText: '외부 기기' });
await guestCard.getByText('승인 대기', { exact: true }).waitFor();
assert.equal(await guestCard.locator('.peer-card').isDisabled(), true);
await guestCard.getByRole('button', { name: '외부 기기 기기 더보기' }).click();
await guestCard.getByRole('menuitem', { name: '참여 승인', exact: true }).click();
await guestCard.getByText('온라인', { exact: true }).waitFor();
assert.equal(await guestCard.locator('.peer-card').isEnabled(), true);
const hostCard = guest.locator('.peer-entry').filter({ hasText: '방장 Mac' });
await hostCard.locator('.host-badge').waitFor();
assert.equal(await guest.getByRole('button', { name: '방장 Mac 기기 더보기' }).count(), 0);
for (const page of [host, guest]) {
assert.equal(await page.locator('.spaces-list,.space-member').count(), 0);
assert(!(await page.locator('body').innerText()).includes('참여 중'));
}
await guestCard.getByRole('button', { name: '외부 기기 기기 더보기' }).click();
const kick = guestCard.getByRole('menuitem', { name: '내보내기', exact: true });
await kick.waitFor();
const rgb = await kick.evaluate((node) => getComputedStyle(node).color);
const components = rgb.match(/\d+/g).map(Number);
assert(components[0] > components[1] && components[0] > components[2]);
await host.keyboard.press('Escape');
assert.equal(await host.locator('.device-menu').count(), 0);
await guestCard.getByRole('button', { name: '외부 기기 기기 더보기' }).click();
await host.locator('.section-heading h2').click();
assert.equal(await host.locator('.device-menu').count(), 0);
await guestCard.getByRole('button', { name: '외부 기기 기기 더보기' }).click();
await host.screenshot({ path: 'tests/e2e/device-menu-desktop.png', fullPage: true });
await guest.screenshot({ path: 'tests/e2e/device-menu-mobile.png', fullPage: true });
await guestCard.getByRole('menuitem', { name: '내보내기', exact: true }).click();
await guestCard.waitFor({ state: 'hidden' });
await hostCard.waitFor({ state: 'hidden' });
assert.equal(await guest.locator('.room-leave').count(), 0);
await host.getByRole('button', { name: '방 나가기', exact: true }).click();
await host.locator('.room-leave').waitFor({ state: 'hidden' });
await host.locator('.my-device-info .host-badge').waitFor({ state: 'hidden' });
assert.equal(
await guest.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
true,
);
assert.deepEqual(errors, []);
console.log(
'PASS: HOST badge on current/peer devices; approval and red kick action in main card menu; non-host has no kick menu; Escape/outside close; no duplicate participant list; kick propagates to both browsers; space close and mobile viewport.',
);
} catch (error) {
console.error(error);
for (const context of browser?.contexts() ?? [])
for (const page of context.pages())
console.error((await page.locator('body').innerText()).slice(0, 4000));
throw error;
} finally {
await browser?.close();
await runtime.app.close();
await rm(folder, { recursive: true, force: true });
}