1import path from "node:path"
2import { expect, type Page } from "@playwright/test"
3
4const FIXTURE_DIR = path.join(process.cwd(), "tests", "e2e", "fixtures")
5
6/* Konvensi navigasi bersama untuk e2e. Berkas ini dipakai capture.spec.ts,
7 customize.spec.ts, dan a11y.spec.ts supaya alur menuju tiap langkah booth
8 ditulis sekali.
9
10 Gesture Snap selalu dimatikan lewat localStorage di alur spesifikasi ini:
11 Chromium headless tanpa GPU butuh ratusan milidetik per inferensi sehingga
12 hitung mundur bisa meleset. Perilaku sakelar menyala (bawaan) diuji berkas
13 `tests/e2e/gesture.spec.ts`. */
14
15export async function matikanGesture(page: Page): Promise<void> {
16 await page.addInitScript(() => {
17 window.localStorage.setItem("snapme:gesture-enabled", "0")
18 })
19}
20
21/** Pilih layout mana pun lalu masuk ke langkah Ambil Foto.
22
23 * Pemilihan radio diulang sampai tombolnya benar-benar menyala: di server
24 * pengembangan yang baru dinyalakan, halaman bisa dikompilasi (dan dimuat
25 * ulang) setelah `goto`, jadi klik pertama kadang jatuh sebelum React
26 * terhidrasi dan tidak mengubah state. */
27async function masukAmbilFotoLayout(
28 page: Page,
29 label: string
30): Promise<void> {
31 await page.goto("/booth/layout")
32 await expect(async () => {
33 await page.getByRole("radio", { name: label }).check()
34 await expect(page.getByRole("button", { name: "Mulai Foto" })).toBeEnabled({
35 timeout: 1_000,
36 })
37 }).toPass({ timeout: 30_000 })
38 await page.getByRole("button", { name: "Mulai Foto" }).click()
39 await page.waitForURL("**/booth/capture")
40}
41
42export async function masukAmbilFoto(page: Page): Promise<void> {
43 await masukAmbilFotoLayout(page, "Strip 4 foto")
44}
45
46export async function izinkanKamera(page: Page): Promise<void> {
47 await page.getByRole("button", { name: "Izinkan Kamera" }).click()
48 await expect(page.locator("video")).toBeVisible()
49 /* Elemen video muncul sebelum izin selesai: tunggu bingkai pertama. */
50 await expect
51 .poll(() =>
52 page.evaluate(() => document.querySelector("video")?.videoWidth ?? 0)
53 )
54 .toBeGreaterThan(0)
55}
56
57export async function jepret(page: Page): Promise<void> {
58 await page.getByRole("button", { name: "Ambil foto" }).click()
59 await expect(page.getByTestId("countdown")).toBeVisible()
60 await expect(page.getByTestId("countdown")).toHaveCount(0)
61}
62
63export async function srcSlot(
64 page: Page,
65 nomor: number
66): Promise<string | null> {
67 return page
68 .getByRole("button", { name: `Ulang foto ${nomor}` })
69 .locator("img")
70 .getAttribute("src")
71}
72
73/** Empat foto dengan kamera palsu, siap dilanjutkan ke langkah Hias. */
74async function lengkapiEmpatFoto(page: Page): Promise<void> {
75 await masukAmbilFoto(page)
76 await izinkanKamera(page)
77 for (let nomor = 0; nomor < 4; nomor += 1) {
78 await jepret(page)
79 }
80 await expect(page.getByText("4/4")).toBeVisible()
81}
82
83export async function masukHias(page: Page): Promise<void> {
84 await lengkapiEmpatFoto(page)
85 await page.getByRole("button", { name: "Lanjut Hias" }).click()
86 await page.waitForURL("**/booth/customize")
87 /* Penyunting dimuat dengan import() dinamis, jadi kanvas baru ada setelah
88 potongan kodenya siap. Menunggunya di sini menjaga tiap uji hias tidak
89 berlomba dengan pemuatan itu. */
90 await expect(page.locator("canvas")).toBeVisible()
91}
92
93/** Hias lalu lanjut ke langkah Hasil, dengan pratinjau hasil sudah siap. */
94export async function masukHasil(page: Page): Promise<void> {
95 await masukHias(page)
96 await page.getByRole("link", { name: "Selesai & Unduh" }).click()
97 await page.waitForURL("**/booth/result")
98 await expect(page.getByAltText("Pratinjau strip")).toBeVisible()
99}
100
101/** M6: hias dengan QR menyala, jalur yang dipakai uji ber-link. */
102export async function masukHiasDenganQr(page: Page): Promise<void> {
103 await masukHias(page)
104 await page.getByRole("switch", { name: "QR di strip" }).click()
105 await expect(
106 page.getByText("Kamu akan dimintai izin unggah saat mengunduh.")
107 ).toBeVisible()
108}
109
110/** FR-13 tanpa kamera: empat berkas galeri mengisi sesi bawaan lalu langsung
111 * masuk langkah Hias. Dipakai uji lintas peramban dan uji kinerja ekspor. */
112export async function masukHiasDariGaleri(page: Page): Promise<void> {
113 await page.goto("/")
114 await expect(page.getByRole("link", { name: "Mulai Foto" }).first()).toBeVisible()
115
116 const chooser = page.waitForEvent("filechooser")
117 await page.getByRole("button", { name: "Pakai Foto Galeri" }).click()
118 await (await chooser).setFiles(
119 ["foto-1.png", "foto-2.png", "foto-3.png", "foto-4.png"].map((nama) =>
120 path.join(FIXTURE_DIR, nama)
121 )
122 )
123 await page.waitForURL("**/booth/customize")
124 await expect(page.getByText("4 foto siap", { exact: true })).toBeVisible()
125 await expect(page.locator("canvas")).toBeVisible()
126}
127
128/** Sidik jari kanvas penyunting: jumlah sampel tiap 97 bita, cukup untuk
129 * membuktikan ada yang berubah tanpa mengangkut seluruh gambar. `-1` berarti
130 * kanvasnya belum ada. Dipakai uji hias, Strip Hidup, dan bagikan. */
131export async function canvasChecksum(page: Page): Promise<number> {
132 return page.evaluate(() => {
133 const canvas = document.querySelector("canvas")
134 if (canvas === null) {
135 return -1
136 }
137 const data = canvas
138 .getContext("2d")
139 ?.getImageData(0, 0, canvas.width, canvas.height).data
140 if (data === undefined) {
141 return -1
142 }
143 let total = 0
144 for (let index = 0; index < data.length; index += 97) {
145 total += data[index]
146 }
147 return total
148 })
149}
150