1import { expect, test } from "@playwright/test"
2
3import { izinkanKamera, jepret, masukAmbilFoto } from "./helpers"
4
5/* M3 PRD §7.2 dengan kamera palsu Chromium (ARCHITECTURE §10):
6 GS-01 (deteksi lokal, aset di-host sendiri), GS-06 (sakelar mati dan diingat),
7 GS-07 (gagal muat), dan HUD selesai. Keadaan `holding` tidak bisa dipicu tanpa
8 tangan sungguhan: dibuktikan uji komponen di tests/features/gesture/gesture-hud.test.tsx.
9 Navigasi dan kamera memakai konvensi bersama di tests/e2e/helpers.ts; sakelar
10 gesture sengaja TIDAK dimatikan di berkas ini. */
11
12const HUD_SIAP = "Angkat dua jari, tahan sebentar"
13
14test("model dimuat dari origin sendiri tanpa permintaan membawa gambar", async ({
15 page,
16}) => {
17 test.setTimeout(90_000)
18
19 const requests: {
20 url: string
21 method: string
22 type: string
23 body: string
24 }[] = []
25 page.on("request", (request) => {
26 requests.push({
27 url: request.url(),
28 method: request.method(),
29 type: request.resourceType(),
30 body: request.postData() ?? "",
31 })
32 })
33
34 await masukAmbilFoto(page)
35 await izinkanKamera(page)
36 /* GS-07: status "Menyiapkan gesture..." boleh terlewat kalau model cepat,
37 jadi yang dijamin adalah HUD siap setelah model termuat. */
38 await expect(page.getByText(HUD_SIAP)).toBeVisible({ timeout: 30_000 })
39
40 for (let nomor = 0; nomor < 4; nomor += 1) {
41 await jepret(page)
42 }
43 await expect(page.getByText("4 foto siap dihias")).toBeVisible()
44
45 /* GS-01: tidak ada permintaan yang membawa gambar. Semua permintaan GET dan
46 seorigin; model serta wasm datang dari /public, bukan CDN. `__nextjs_`
47 adalah diagnostik Next dev yang muncul saat MediaPipe menulis peringatan ke
48 konsol, bukan bagian dari alur foto. */
49 const halaman = requests.filter(
50 (request) =>
51 request.type !== "websocket" && !request.url.includes("/__nextjs_")
52 )
53 expect(
54 halaman
55 .filter((request) => request.method !== "GET")
56 .map((request) => `${request.method} ${request.url}`)
57 ).toEqual([])
58 expect(
59 halaman
60 .filter(
61 (request) => request.body.includes("data:image") || request.body.includes("/9j/")
62 )
63 .map((request) => request.url)
64 ).toEqual([])
65 expect(
66 halaman.filter((request) => !request.url.startsWith("http://localhost:3000"))
67 ).toEqual([])
68 expect(
69 halaman.some((request) =>
70 request.url.includes("/models/gesture_recognizer.task")
71 )
72 ).toBe(true)
73 expect(
74 halaman.some((request) => request.url.includes("/mediapipe/wasm/"))
75 ).toBe(true)
76})
77
78test("sakelar mati: model tidak dimuat dan HUD menyebut jalan tombol", async ({
79 page,
80}) => {
81 const requests: string[] = []
82 page.on("request", (request) => requests.push(request.url()))
83
84 await masukAmbilFoto(page)
85 const toggle = page.getByRole("button", { name: /Gesture Snap/ })
86 await toggle.click()
87 await expect(toggle).toHaveAttribute("aria-pressed", "false")
88
89 await izinkanKamera(page)
90 await expect(
91 page.getByText("Gesture mati. Pakai tombol kamera.")
92 ).toBeVisible()
93 await page.waitForTimeout(1500)
94
95 expect(
96 requests.filter(
97 (url) => url.includes("/models/") || url.includes("/mediapipe/")
98 )
99 ).toEqual([])
100})
101
102test("pilihan sakelar diingat setelah halaman dimuat ulang", async ({ page }) => {
103 await masukAmbilFoto(page)
104 const toggle = page.getByRole("button", { name: /Gesture Snap/ })
105 await toggle.click()
106 await expect(toggle).toHaveAttribute("aria-pressed", "false")
107
108 /* FR-14 menyimpan sesi dengan jeda 400 ms; tunggu tulisannya selesai supaya
109 muat ulang di bawah benar-benar menguji ingatan sakelar. Sesi foto ikut
110 lintas muat ulang, jadi halaman tetap di langkah yang sama. */
111 await page.waitForTimeout(600)
112 await page.reload()
113 await expect(page).toHaveURL(/\/booth\/capture$/)
114
115 await expect(
116 page.getByRole("button", { name: /Gesture Snap/ })
117 ).toHaveAttribute("aria-pressed", "false")
118})
119
120test("model gagal dimuat: sakelar hilang, toast tampil, tombol kamera tetap jalan", async ({
121 page,
122}) => {
123 await page.route("**/models/**", (route) => route.abort())
124
125 await masukAmbilFoto(page)
126 await izinkanKamera(page)
127
128 await expect(
129 page.getByText(
130 "Gesture Snap gagal dimuat. Tombol kamera tetap bisa dipakai."
131 )
132 ).toBeVisible({ timeout: 30_000 })
133 await expect(page.getByRole("button", { name: /Gesture Snap/ })).toHaveCount(0)
134
135 await jepret(page)
136 await expect(page.getByText("1/4")).toBeVisible()
137})
138