1import { readFileSync } from "node:fs"
2import { expect, test } from "@playwright/test"
3
4import {
5 izinkanKamera,
6 jepret,
7 masukHasil,
8 matikanGesture,
9} from "./helpers"
10
11/* M5: halaman Hasil, unduh PNG (FR-11), dan cetak 4R (FR-12).
12 Konvensi navigasi ada di tests/e2e/helpers.ts. */
13
14const PNG_BYTE_LIMIT = 2_097_152
15const ORIGIN = "http://localhost:3000"
16const HALAMAN_APP = [
17 "/",
18 "/privasi",
19 "/booth/layout",
20 "/booth/capture",
21 "/booth/customize",
22 "/booth/result",
23]
24const AWALAN_STATIS = [
25 "/_next/",
26 /* Rute fon server pengembangan Next.js: tetap berkas statis milik aplikasi. */
27 "/__nextjs_font/",
28 "/brand/",
29 "/mediapipe/",
30 "/models/",
31 "/stickers/",
32 "/favicon.ico",
33 "/icon.svg",
34 "/apple-icon.png",
35]
36/* M9 (PRD §4): satu-satunya rute server yang boleh disentuh alur dasar adalah
37 analitik first-party. Isinya hanya nama peristiwa, tanpa gambar dan tanpa
38 data pribadi, jadi uji privasi di bawah tetap berlaku untuk fotonya. */
39const RUTE_DIIZINKAN = ["/api/events"]
40
41test.beforeEach(async ({ page }) => {
42 await matikanGesture(page)
43})
44
45/** Gambar PNG: delapan bita tanda tangan lalu IHDR dengan lebarnya di offset 16. */
46function periksaPng(bytes: Buffer, lebar: number, tinggi: number): void {
47 expect(bytes.subarray(0, 8).toString("hex")).toBe("89504e470d0a1a0a")
48 expect(bytes.toString("ascii", 12, 16)).toBe("IHDR")
49 expect(bytes.readUInt32BE(16)).toBe(lebar)
50 expect(bytes.readUInt32BE(20)).toBe(tinggi)
51 expect(bytes.byteLength).toBeLessThan(PNG_BYTE_LIMIT)
52}
53
54/** Jumlah halaman dari keluaran `page.pdf` Chromium. */
55function jumlahHalaman(pdf: Buffer): number {
56 return (pdf.toString("latin1").match(/\/Type\s*\/Page[^s]/g) ?? []).length
57}
58
59test("alur penuh Welcome sampai unduh tanpa permintaan non-statis", async ({
60 page,
61}) => {
62 const requests: string[] = []
63 page.on("request", (request) => requests.push(request.url()))
64
65 await page.goto("/")
66 await page.getByRole("link", { name: "Mulai Foto" }).first().click()
67 await page.waitForURL("**/booth/layout")
68
69 await page.getByRole("radio", { name: "Strip 4 foto" }).check()
70 await page.getByRole("button", { name: "Mulai Foto" }).click()
71 await page.waitForURL("**/booth/capture")
72
73 await izinkanKamera(page)
74 for (let nomor = 0; nomor < 4; nomor += 1) {
75 await jepret(page)
76 }
77
78 await page.getByRole("button", { name: "Lanjut Hias" }).click()
79 await page.waitForURL("**/booth/customize")
80 await expect(page.locator("canvas")).toBeVisible()
81
82 await page.getByRole("link", { name: "Selesai & Unduh" }).click()
83 await page.waitForURL("**/booth/result")
84 await expect(page.getByAltText("Pratinjau strip")).toBeVisible()
85
86 const [download] = await Promise.all([
87 page.waitForEvent("download"),
88 page.getByRole("button", { name: "Selesai & Unduh" }).click(),
89 ])
90
91 await expect(page.getByText("Strip tersimpan di perangkatmu.")).toBeVisible()
92 expect(download.suggestedFilename()).toMatch(/^snapme-\d{8}-\d{4}\.png$/)
93
94 const berkas = await download.path()
95 if (berkas === null) {
96 throw new Error("Berkas unduhan tidak tersedia di disk")
97 }
98 periksaPng(readFileSync(berkas), 600, 1800)
99
100 /* FR-11 dan PRD GS-01: seluruh alur dasar hanya boleh menyentuh aset statis
101 dan halaman aplikasi, tanpa satu pun permintaan ke rute server. */
102 const diLuarAturan = requests.filter((url) => {
103 const parsed = new URL(url)
104 if (parsed.protocol === "data:" || parsed.protocol === "blob:") {
105 return false
106 }
107 if (parsed.origin !== ORIGIN) {
108 return true
109 }
110 if (parsed.pathname.startsWith("/api/")) {
111 return !RUTE_DIIZINKAN.includes(parsed.pathname)
112 }
113 return !(
114 HALAMAN_APP.includes(parsed.pathname) ||
115 AWALAN_STATIS.some((awalan) => parsed.pathname.startsWith(awalan))
116 )
117 })
118 expect(diLuarAturan).toEqual([])
119})
120
121test("pratinjau cetak satu halaman 4R", async ({ page }) => {
122 await masukHasil(page)
123
124 const lembar = page.locator("[data-print-sheet]")
125 const gambar = page.locator("[data-print-sheet] img")
126
127 await expect(lembar).toBeHidden()
128 await expect
129 .poll(() =>
130 gambar.evaluate((node) => (node as HTMLImageElement).naturalWidth)
131 )
132 .toBeGreaterThan(0)
133
134 await page.emulateMedia({ media: "print" })
135 await expect(lembar).toBeVisible()
136
137 /* 4 x 6 inci pada 96 dpi = 384 x 576 px. */
138 const kotak = await gambar.boundingBox()
139 if (kotak === null) {
140 throw new Error("Lembar cetak tidak terukur")
141 }
142 expect(Math.round(kotak.width)).toBe(384)
143 expect(Math.round(kotak.height)).toBe(576)
144
145 const pdf = await page.pdf({ preferCSSPageSize: true })
146 expect(jumlahHalaman(pdf)).toBe(1)
147
148 await page.emulateMedia({ media: null })
149})
150
151test("Mulai dari awal mengosongkan sesi", async ({ page }) => {
152 await masukHasil(page)
153
154 await page.getByRole("button", { name: "Mulai dari awal" }).click()
155 await page.waitForURL("**/booth/layout")
156 await expect(page.getByText("Belum dipilih")).toBeVisible()
157
158 await page.goto("/booth/capture")
159 await page.waitForURL("**/booth/layout")
160})
161
162/* FR-03: Grid 2 x 2 diekspor 1200 x 1800 dan dicetak sebagai satu lembar 4R. */
163test("Grid 2 x 2 diekspor 1200 x 1800 dan dicetak satu halaman", async ({
164 page,
165}) => {
166 await page.goto("/booth/layout")
167 await page.getByRole("radio", { name: "Grid 2 x 2" }).check()
168 await expect(page.getByText("4 foto, ekspor 1200 x 1800 px.")).toBeVisible()
169 await expect(
170 page.getByText("Ekspor 1200 x 1800 px", { exact: true })
171 ).toBeVisible()
172 await page.getByRole("button", { name: "Mulai Foto" }).click()
173 await page.waitForURL("**/booth/capture")
174
175 await izinkanKamera(page)
176 for (let nomor = 0; nomor < 4; nomor += 1) {
177 await jepret(page)
178 }
179
180 await page.getByRole("button", { name: "Lanjut Hias" }).click()
181 await page.waitForURL("**/booth/customize")
182 await expect(page.locator("canvas")).toBeVisible()
183 await expect(
184 page.getByText("Ekspor 1200 x 1800 px (4 x 6 inci, 300 dpi)")
185 ).toBeVisible()
186
187 await page.getByRole("link", { name: "Selesai & Unduh" }).click()
188 await page.waitForURL("**/booth/result")
189 await expect(page.getByAltText("Pratinjau strip")).toBeVisible()
190 await expect(page.getByText("Ekspor 1200 x 1800 px", { exact: true })).toBeVisible()
191
192 const [download] = await Promise.all([
193 page.waitForEvent("download"),
194 page.getByRole("button", { name: "Selesai & Unduh" }).click(),
195 ])
196
197 const berkas = await download.path()
198 if (berkas === null) {
199 throw new Error("Berkas unduhan tidak tersedia di disk")
200 }
201 periksaPng(readFileSync(berkas), 1200, 1800)
202
203 await page.emulateMedia({ media: "print" })
204 await expect(page.locator("[data-print-sheet]")).toBeVisible()
205 const pdf = await page.pdf({ preferCSSPageSize: true })
206 expect(jumlahHalaman(pdf)).toBe(1)
207 await page.emulateMedia({ media: null })
208})
209