Mengakali Batasan Secure Cookie di WebKit untuk Playwright Testing di Localhost
Wed, March 25, 2026 — 6 min read
This article is available in other language(s): English
Cookie memiliki salah satu atribut yang cukup penting, yaitu Secure. Atribut Secure ini adalah sebuah opsi yang bisa dipasang dari server ketika mengirim sebuah cookie ke pengguna melalui HTTP. Tujuan dari atribut ini adalah untuk mencegah cookie bisa dilihat oleh orang yang tidak bertanggung jawab karena cookie dikirim dalam bentuk teks biasa—sehingga, browser yang mendukung atribut Secure ini hanya akan mengirim/memasang cookie ketika request ke sebuah halaman atau resource yang menggunakan HTTPS (terenkripsi).
Pada implementasinya, jika kalian membuat sebuah aplikasi web yang dapat berjalan di semua browser, akan terdapat masalah ketika mencobanya di browser dengan engine WebKit, seperti Safari. Salah satu laporan bug menyebutkan bahwa terdapat perilaku yang inkonsisten pada WebKit dalam menentukan apakah localhost termasuk ke dalam secure origin atau bukan. Laporan tersebut juga didukung oleh status konsensus dan standarisasi fitur untuk memperlakukan localhost atau loopback address sebagai secure context pada Chrome Platform Status yang menyatakan bahwa WebKit masih belum mendukungnya.
Untuk mengatasi masalah tersebut selama pengembangan, setidaknya ada beberapa cara yang bisa dilakukan, seperti:
- Mengkonfigurasi HTTPS untuk pengembangan di lokal
- Mengirim
Secureatribut secara kondisional berdasarkan origin dari request - Memasang proxy server untuk mengubah cookie sebelum dikirim ke client
Jika kalian mencoba contoh yang paling sederhana, yaitu mengkonfigurasi HTTPS di lokal, mungkin sekarang secure cookie kalian sudah bisa terpasang dengan baik di Safari. Namun, diperlukan konfigurasi berbeda jika kalian menggunakan end-to-end, cross-browser testing seperti Playwright di CI/CD kalian. Well, sangat mungkin kita melakukan hal yang sama untuk mengkonfigurasi HTTPS di CI/CD seperti yang disebutkan di artikel ini—hanya saja, kita memerlukan hal yang lebih sederhana yaitu tetap menggunakan localhost tanpa HTTPS dan mengkonfigurasi test suites yang kita punya, pada kasus kali ini menggunakan Playwright.
Untuk E2E framework lain seperti Cypress, seharusnya kalian juga bisa menyesuaikan dengan mencari konfigurasi padanannya.
Kasus yang menjadi contoh untuk artikel ini adalah pengujian end-to-end untuk aplikasi to-do list yang saya buat di mana autentikasinya menggunakan HttpOnly dan Secure cookie yang akan gagal ketika dijalankan di Safari. Untuk ini, kita bisa memanfaatkan project dependencies dan membuat setup untuk masing-masing browser, yang mana setiap browser akan memiliki akun pengguna yang berbeda.
Mengenal Projects di Playwright
Playwright memiliki satu hal yang bernama project. Project ini adalah cara untuk mengelompokkan pengujian yang akan berjalan dengan konfigurasi yang sama. Projects digunakan untuk menjalankan pengujian pada browser dan perangkat yang berbeda. Inilah yang digunakan untuk menguji apakah aplikasi web kita bisa berjalan normal di mayoritas browser (Chrome, Firefox, dan Safari).
Berikut adalah contoh konfigurasi projects di Playwright:
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
{
name: "Mobile Chrome",
use: { ...devices["Pixel 5"] },
},
{
name: "Mobile Safari",
use: { ...devices["iPhone 12"] },
},
/* Test against branded browsers. */
{
name: "Microsoft Edge",
use: {
...devices["Desktop Edge"],
channel: "msedge",
},
},
{
name: "Google Chrome",
use: {
...devices["Desktop Chrome"],
channel: "chrome",
},
},
],
});Selain itu, ada juga project dependencies yang biasa digunakan untuk setup sebelum menjalankan semua tests pada project. Setup ini berguna untuk mengkonfigurasi autentikasi.
Membuat setup untuk masing-masing browser
Langkah pertama, kita perlu membuat setup untuk masing-masing browser. Setup ini sebenarnya hanya menggunakan abstraksi dari setup autentikasi yang di mana alur autentikasi didefinisikan di sana, hanya saja kita memerlukan tempat penyimpanan yang berbeda untuk masing-masing browser dan untuk memodifikasi perilaku browser tertentu—pada kasus ini, Safari.
Buatlah beberapa berkas setup seperti berikut:
my-app/
├─ tests/
│ ├─ auth.setup.ts
│ ├─ chromium.setup.ts
│ ├─ firefox.setup.ts
│ ├─ webkit.setup.tsMenjadikan setup autentikasi sebagai abstraksi
Pada berkasi auth.setup.ts, kita akan membuat sebuah fungsi yang akan mengabstraksi keseluruhan alur autentikasi ditambah dengan sebuah callback untuk menambahkan modifikasi di alur autentikasi jika diperlukan.
import { test as setup, expect, type Page } from "@playwright/test";
type SetupAuth = {
email: string;
password: string;
authStorePath: string;
alterStoreCallback?: ({ page }: { page: Page }) => Promise<void>;
};
export function setupAuth({ email, password, authStorePath, alterStoreCallback }: SetupAuth) {
setup.describe.configure({ mode: "serial" });
setup.describe("Authentication", () => {
setup("Register", async ({ page }) => {
await page.goto("/register");
await page.getByLabel("Email").fill(email);
await page.getByLabel("Password").fill(password);
await page.getByRole("button", { name: "Create account" }).click();
await page.waitForURL("/login");
await expect(page.getByText("Login to your account", { exact: true })).toBeVisible();
});
setup("Login", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill(email);
await page.getByLabel("Password").fill(password);
await page.getByRole("button", { name: "Login" }).click();
await page.waitForURL("/todo");
await expect(page.getByText("To-Do", { exact: true })).toBeVisible();
await expect(page.getByText("Please, do something.", { exact: true })).toBeVisible();
await alterStoreCallback?.({ page });
await page.context().storageState({ path: authStorePath });
});
});
}Fungsi setupAuth akan diekspor dan digunakan di dalam berkas setup untuk masing-masing browser. Ini memungkinkan kita untuk mendefinisikan di mana Playwright akan menyimpan auth state untuk tiap browser, juga untuk memodifikasi auth state tersebut.
Memperbarui playwright.config.ts
Kita perlu memastikan bahwa masing-masing projects sudah dipasangkan dengan dependency-nya yang sesuai.
import path from "node:path";
import { defineConfig, devices } from "@playwright/test";
export const CHROMIUM_AUTH_FILE = path.join(import.meta.dirname, "./.auth/user-chromium.json");
export const FIREFOX_AUTH_FILE = path.join(import.meta.dirname, "./.auth/user-firefox.json");
export const WEBKIT_AUTH_FILE = path.join(import.meta.dirname, "./.auth/user-webkit.json");
export default defineConfig({
/* Other configuration is hidden for conciseness */
/* Configure projects for major browsers */
projects: [
// { name: "setup", testMatch: /.*\.setup\.ts/ },
//--begin-chromium
{
name: "chromium-setup",
testMatch: /chromium.setup.ts/,
use: { ...devices["Desktop Chrome"] },
},
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
storageState: CHROMIUM_AUTH_FILE,
},
dependencies: ["chromium-setup"],
},
//--end-chromium
//--begin-firefox
{
name: "firefox-setup",
testMatch: /firefox.setup.ts/,
use: { ...devices["Desktop Firefox"] },
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
storageState: FIREFOX_AUTH_FILE,
},
dependencies: ["firefox-setup"],
},
//--end-firefox
//--begin-webkit
// setup using chromium to get the secure cookies and alter it later
{
name: "webkit-setup",
testMatch: /webkit.setup.ts/,
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
storageState: WEBKIT_AUTH_FILE,
},
dependencies: ["webkit-setup"],
},
//--end-webkit
],
});Bisa kita lihat, di atas konfigurasi tersebut terdapat konstanta yang mendefinisikan di mana auth state untuk masing-masing project akan disimpan. Konstanta tersebut juga diekspor agar bisa digunakan di tiap berkas setup.
webkit-setup akan dijalankan sebelum menjalankan project bernama webkit yang mana menggunakan Safari versi desktop. Pada berkas webkit.setup.ts nanti kita akan memodifikasi cookie yang disimpan agar bisa berjalan di localhost, yaitu dengan mematikan atribut Secure.
Setup untuk masing-masing browser
Untuk Chrome dan Firefox, sederhana saja, berkas setup-nya akan terlihat seperti ini:
import { CHROMIUM_AUTH_FILE } from "../../playwright.config";
import { setupAuth } from "./auth.setup";
setupAuth({
email: "[email protected]",
password: "chromium",
authStorePath: CHROMIUM_AUTH_FILE,
});import { FIREFOX_AUTH_FILE } from "../../playwright.config";
import { setupAuth } from "./auth.setup";
setupAuth({
email: "[email protected]",
password: "firefox",
authStorePath: FIREFOX_AUTH_FILE,
});Khusus untuk Safari (WebKit), kita akan memanfaatkan parameter callback yang sudah disediakan oleh setupAuth untuk memodifikasi cookie yang diterima.
import { WEBKIT_AUTH_FILE } from "../../playwright.config";
import { setupAuth } from "./auth.setup";
setupAuth({
email: "[email protected]",
password: "webkit",
authStorePath: WEBKIT_AUTH_FILE,
async alterStoreCallback({ page }) {
/**
* workaround to make `Set-Cookie` work in unsecure context (localhost) for webkit.
* https://chromestatus.com/feature/6269417340010496
* https://bugs.webkit.org/show_bug.cgi?id=281149
*/
const storageState = await page.context().storageState();
const cookies = storageState.cookies;
const unsecureCookies = cookies.map(cookie => ({
...cookie,
secure: false,
}));
await page.context().addCookies(unsecureCookies);
},
});alterStoreCallback dipanggil sesaat sebelum Playwright menyimpan page context, sehingga cookie yang dimodifikasi dapat digunakan dalam pengujian.
Jika ini tidak dilakukan, maka pengujian di Safari akan selalu gagal karena WebKit (engine yang digunakan di Safari) tidak menyimpan cookie dengan atribut Secure di localhost atau tanpa HTTPS. Oleh karena itu, untuk pengujian, kita perlu memodifikasi cookie yang disimpan dan hanya pada Safari, agar semua atribut Secure pada cookie dimatikan.