qaify-playwright-reporter 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,15 +10,21 @@ Rancangan: [03 · Struktur Backend §7](../03-struktur-backend.md#7-reporter-pla
10
10
  2. Di repo:
11
11
 
12
12
  ```bash
13
- npm i -D qaify-playwright-reporter
14
- npx qaify init # tulis .github/workflows/qa.yml (tidak menimpa; --force untuk menimpa)
13
+ npm i -D @playwright/test qaify-playwright-reporter
14
+ npx qaify init
15
15
  ```
16
16
 
17
+ `qaify init` tidak pernah menimpa file yang sudah ada (kecuali `qa.yml` dengan `--force`):
18
+ - `.github/workflows/qa.yml`, dari [`templates/qa.yml`](templates/qa.yml);
19
+ - bila belum ada config Playwright: `playwright.config.ts` dengan `testDir: tests/e2e` dan `testMatch: *.e2e.ts`, sehingga unit test `*.test.ts` milik Vitest/Jest tidak ikut. Reporter QA Factory sudah terpasang, dan `webServer` dideteksi dari `package.json` (Vite, Next, Nuxt, Astro, CRA);
20
+ - bila belum ada test e2e: contoh `tests/e2e/smoke.e2e.ts` ("halaman utama terbuka").
21
+ - Repo Laravel atau yang tidak dikenali tidak diberi `webServer`. Isi `BASE_URL` atau tambahkan `webServer` sendiri; sampai saat itu contoh test di-skip, tetapi tetap hijau.
22
+
17
23
  3. Buat secret `QA_FACTORY_TOKEN`, variable `QA_FACTORY_URL`, dan opsional secret `CLAUDE_CODE_OAUTH_TOKEN` (lihat di bawah), lalu push.
18
24
 
19
- Template ada di [`templates/qa.yml`](templates/qa.yml). Bila aplikasi butuh database dummy, sediakan script `npm run db:seed`; tanpa script itu langkahnya dilewati.
25
+ Bila aplikasi butuh database dummy, sediakan script `npm run db:seed`; tanpa script itu langkahnya dilewati.
20
26
 
21
- `playwright.config.ts`:
27
+ Sudah punya `playwright.config.ts`? `qaify init` tidak mengubahnya. Tambahkan reporter sendiri:
22
28
 
23
29
  ```ts
24
30
  reporter: [
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Sekali di repo yang diuji:
3
- // qaify init [--force] tulis .github/workflows/qa.yml
3
+ // qaify init [--force] tulis qa.yml (+ playwright.config.ts & contoh test bila belum ada)
4
4
  // qaify check cek URL, token, dan repo pemilik token (awal job test)
5
5
  // Job analyze di qa.yml (bin lama `playwright-reporter` tetap bisa dipakai):
6
6
  // qaify analyze --conclusion "${{ needs.test.result }}"
@@ -11,7 +11,7 @@ import { appendFile, readFile, writeFile } from 'node:fs/promises';
11
11
  import { INSTRUCTIONS, RESULT_SCHEMA } from './ai.js';
12
12
  import { aiQueue, aiSubmit, analyze, CONCLUSIONS, parseArgs } from './analyze.js';
13
13
  import { createClient, describeProblem, MISSING_CONFIG, runFromEnv, warn } from './client.js';
14
- import { init, NEXT_STEPS, WORKFLOW_PATH } from './init.js';
14
+ import { init, NEXT_STEPS } from './init.js';
15
15
  import { check } from './check.js';
16
16
  const USAGE = `Pemakaian:
17
17
  qaify init [--force]
@@ -26,8 +26,10 @@ const run = runFromEnv();
26
26
  // semua perintah exit 0 kecuali argumen salah: platform bermasalah tidak boleh membuat workflow merah
27
27
  switch (args.command) {
28
28
  case 'init': {
29
- const { written } = await init(process.cwd(), args.force);
30
- console.log(written ? `qa-factory: ${WORKFLOW_PATH} dibuat` : `qa-factory: ${WORKFLOW_PATH} sudah ada, tidak ditimpa (pakai --force)`);
29
+ for (const action of await init(process.cwd(), args.force)) {
30
+ const mark = { created: '+', skipped: '=', hint: '!' }[action.kind];
31
+ console.log(`qa-factory: ${mark} ${action.message}`);
32
+ }
31
33
  console.log(`\n${NEXT_STEPS}`);
32
34
  break;
33
35
  }
package/dist/init.d.ts CHANGED
@@ -1,7 +1,32 @@
1
1
  export declare const WORKFLOW_PATH: string;
2
- export declare const NEXT_STEPS = "Langkah berikutnya:\n 1. playwright.config.ts:\n reporter: [['list'], ['qaify-playwright-reporter']],\n use: { trace: 'retain-on-failure', video: 'retain-on-failure', screenshot: 'only-on-failure' },\n Nama project selain chromium/firefox/webkit: ['qaify-playwright-reporter', { browsers: { 'Mobile Safari': 'webkit' } }]\n 2. GitHub \u2192 Settings \u2192 Secrets and variables \u2192 Actions:\n secret QA_FACTORY_TOKEN (Pengaturan \u2192 Token Reporter di QA Factory)\n variable QA_FACTORY_URL (URL API QA Factory)\n secret CLAUDE_CODE_OAUTH_TOKEN (opsional: `claude setup-token`, analisis AI dengan langganan Claude Anda)\n 3. Commit .github/workflows/qa.yml lalu push.";
3
- /** Tulis .github/workflows/qa.yml dari template; file yang sudah ada tidak ditimpa kecuali force. */
4
- export declare function init(cwd: string, force?: boolean): Promise<{
5
- path: string;
6
- written: boolean;
7
- }>;
2
+ export declare const CONFIG_PATH = "playwright.config.ts";
3
+ export declare const E2E_DIR: string;
4
+ export declare const SMOKE_PATH: string;
5
+ export declare const REPORTER_SNIPPET = "reporter: [['list'], ['qaify-playwright-reporter']],\nuse: { trace: 'retain-on-failure', video: 'retain-on-failure', screenshot: 'only-on-failure' },";
6
+ export declare const NEXT_STEPS = "Langkah berikutnya:\n 1. GitHub \u2192 Settings \u2192 Secrets and variables \u2192 Actions:\n secret QA_FACTORY_TOKEN (Pengaturan \u2192 Token Reporter di QA Factory)\n variable QA_FACTORY_URL (URL API QA Factory, tab Variables)\n secret CLAUDE_CODE_OAUTH_TOKEN (opsional: `claude setup-token`, analisis AI dengan langganan Claude Anda)\n 2. Cek lokal (opsional): npx playwright install chromium && npx playwright test\n 3. Commit file di atas lalu push.";
7
+ export interface InitAction {
8
+ kind: 'created' | 'skipped' | 'hint';
9
+ path?: string;
10
+ message: string;
11
+ }
12
+ export interface WebServer {
13
+ command: string;
14
+ port: number;
15
+ }
16
+ interface PackageJson {
17
+ scripts?: Record<string, string>;
18
+ dependencies?: Record<string, string>;
19
+ devDependencies?: Record<string, string>;
20
+ }
21
+ /**
22
+ * Perintah menyalakan aplikasi untuk webServer Playwright, dari package.json.
23
+ * null: tidak dikenali, atau Laravel (`npm run dev` di sana hanya server aset, bukan aplikasinya).
24
+ */
25
+ export declare function detectWebServer(pkg: PackageJson, isLaravel: boolean): WebServer | null;
26
+ export declare function playwrightConfig(server: WebServer | null): string;
27
+ /**
28
+ * Siapkan repo untuk QA Factory: workflow qa.yml, dan bila belum ada Playwright, config + contoh test.
29
+ * File yang sudah ada tidak pernah diubah (kecuali qa.yml dengan force); config milik pengguna hanya diberi catatan.
30
+ */
31
+ export declare function init(cwd: string, force?: boolean): Promise<InitAction[]>;
32
+ export {};
package/dist/init.js CHANGED
@@ -1,25 +1,133 @@
1
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { access, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
3
  export const WORKFLOW_PATH = join('.github', 'workflows', 'qa.yml');
4
- // dist/init.js → ../templates/qa.yml (ikut terbit lewat "files" di package.json)
5
- const TEMPLATE = new URL('../templates/qa.yml', import.meta.url);
4
+ export const CONFIG_PATH = 'playwright.config.ts';
5
+ export const E2E_DIR = join('tests', 'e2e');
6
+ export const SMOKE_PATH = join(E2E_DIR, 'smoke.e2e.ts');
7
+ // dist/init.js → ../templates/* (ikut terbit lewat "files" di package.json)
8
+ const WORKFLOW_TEMPLATE = new URL('../templates/qa.yml', import.meta.url);
9
+ const SMOKE_TEMPLATE = new URL('../templates/smoke.e2e.ts', import.meta.url);
10
+ const CONFIG_FILES = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs', 'playwright.config.cjs'];
11
+ export const REPORTER_SNIPPET = `reporter: [['list'], ['qaify-playwright-reporter']],
12
+ use: { trace: 'retain-on-failure', video: 'retain-on-failure', screenshot: 'only-on-failure' },`;
6
13
  export const NEXT_STEPS = `Langkah berikutnya:
7
- 1. playwright.config.ts:
8
- reporter: [['list'], ['qaify-playwright-reporter']],
9
- use: { trace: 'retain-on-failure', video: 'retain-on-failure', screenshot: 'only-on-failure' },
10
- Nama project selain chromium/firefox/webkit: ['qaify-playwright-reporter', { browsers: { 'Mobile Safari': 'webkit' } }]
11
- 2. GitHub → Settings → Secrets and variables → Actions:
14
+ 1. GitHub → Settings → Secrets and variables → Actions:
12
15
  secret QA_FACTORY_TOKEN (Pengaturan → Token Reporter di QA Factory)
13
- variable QA_FACTORY_URL (URL API QA Factory)
16
+ variable QA_FACTORY_URL (URL API QA Factory, tab Variables)
14
17
  secret CLAUDE_CODE_OAUTH_TOKEN (opsional: \`claude setup-token\`, analisis AI dengan langganan Claude Anda)
15
- 3. Commit .github/workflows/qa.yml lalu push.`;
16
- /** Tulis .github/workflows/qa.yml dari template; file yang sudah ada tidak ditimpa kecuali force. */
18
+ 2. Cek lokal (opsional): npx playwright install chromium && npx playwright test
19
+ 3. Commit file di atas lalu push.`;
20
+ const exists = (path) => access(path).then(() => true, () => false);
21
+ /**
22
+ * Perintah menyalakan aplikasi untuk webServer Playwright, dari package.json.
23
+ * null: tidak dikenali, atau Laravel (`npm run dev` di sana hanya server aset, bukan aplikasinya).
24
+ */
25
+ export function detectWebServer(pkg, isLaravel) {
26
+ if (isLaravel)
27
+ return null;
28
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
29
+ const scripts = pkg.scripts ?? {};
30
+ if (scripts.dev) {
31
+ if (deps.next)
32
+ return { command: 'npm run dev -- -p 3000', port: 3000 };
33
+ if (deps.nuxt)
34
+ return { command: 'npm run dev -- --port 3000', port: 3000 };
35
+ if (deps.astro)
36
+ return { command: 'npm run dev -- --port 4321', port: 4321 };
37
+ if (deps.vite || scripts.dev.includes('vite'))
38
+ return { command: 'npm run dev -- --port 5173 --strictPort', port: 5173 };
39
+ }
40
+ if (deps['react-scripts'] && scripts.start)
41
+ return { command: 'npm start', port: 3000 };
42
+ return null;
43
+ }
44
+ export function playwrightConfig(server) {
45
+ const baseURL = server ? `process.env.BASE_URL ?? 'http://localhost:${server.port}'` : 'process.env.BASE_URL';
46
+ const webServer = server
47
+ ? `
48
+ // menyalakan aplikasi sebelum test; lokal memakai server yang sudah jalan bila ada
49
+ webServer: {
50
+ command: '${server.command}',
51
+ url: 'http://localhost:${server.port}',
52
+ reuseExistingServer: !process.env.CI,
53
+ timeout: 120_000,
54
+ },`
55
+ : `
56
+ // aplikasi tidak dinyalakan otomatis: isi BASE_URL, atau tambahkan webServer
57
+ // (https://playwright.dev/docs/test-webserver)`;
58
+ return `import { defineConfig, devices } from '@playwright/test'
59
+
60
+ // Dibuat oleh \`npx qaify init\`. Test e2e: tests/e2e/*.e2e.ts — unit test (*.test.ts, Vitest/Jest) tidak ikut.
61
+ // Hasil dikirim ke QA Factory oleh qaify-playwright-reporter (diam bila QA_FACTORY_URL / QA_FACTORY_TOKEN kosong).
62
+ export default defineConfig({
63
+ testDir: './tests/e2e',
64
+ testMatch: '**/*.e2e.ts',
65
+ forbidOnly: !!process.env.CI,
66
+ retries: process.env.CI ? 1 : 0,
67
+ reporter: [['list'], ['qaify-playwright-reporter']],
68
+ use: {
69
+ baseURL: ${baseURL},
70
+ trace: 'retain-on-failure',
71
+ video: 'retain-on-failure',
72
+ screenshot: 'only-on-failure',
73
+ },
74
+ projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],${webServer}
75
+ })
76
+ `;
77
+ }
78
+ async function write(cwd, path, content) {
79
+ await mkdir(dirname(join(cwd, path)), { recursive: true });
80
+ await writeFile(join(cwd, path), content);
81
+ }
82
+ /**
83
+ * Siapkan repo untuk QA Factory: workflow qa.yml, dan bila belum ada Playwright, config + contoh test.
84
+ * File yang sudah ada tidak pernah diubah (kecuali qa.yml dengan force); config milik pengguna hanya diberi catatan.
85
+ */
17
86
  export async function init(cwd, force = false) {
18
- const path = join(cwd, WORKFLOW_PATH);
19
- const exists = await access(path).then(() => true, () => false);
20
- if (exists && !force)
21
- return { path, written: false };
22
- await mkdir(dirname(path), { recursive: true });
23
- await writeFile(path, await readFile(TEMPLATE, 'utf8'));
24
- return { path, written: true };
87
+ const actions = [];
88
+ // 1. workflow
89
+ if ((await exists(join(cwd, WORKFLOW_PATH))) && !force) {
90
+ actions.push({ kind: 'skipped', path: WORKFLOW_PATH, message: `${WORKFLOW_PATH} sudah ada, tidak ditimpa (pakai --force)` });
91
+ }
92
+ else {
93
+ await write(cwd, WORKFLOW_PATH, await readFile(WORKFLOW_TEMPLATE, 'utf8'));
94
+ actions.push({ kind: 'created', path: WORKFLOW_PATH, message: `${WORKFLOW_PATH} dibuat` });
95
+ }
96
+ const pkg = JSON.parse(await readFile(join(cwd, 'package.json'), 'utf8').catch(() => '{}'));
97
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
98
+ // 2. config Playwright
99
+ const existing = (await Promise.all(CONFIG_FILES.map(async (f) => ((await exists(join(cwd, f))) ? f : null)))).find(Boolean);
100
+ if (existing) {
101
+ actions.push({ kind: 'skipped', path: existing, message: `${existing} sudah ada, tidak diubah` });
102
+ const content = await readFile(join(cwd, existing), 'utf8');
103
+ if (!content.includes('qaify-playwright-reporter')) {
104
+ actions.push({
105
+ kind: 'hint',
106
+ path: existing,
107
+ message: `tambahkan reporter QA Factory di ${existing}:\n${REPORTER_SNIPPET}\nPastikan testDir hanya menunjuk folder test e2e, agar unit test (Vitest/Jest) tidak ikut dijalankan Playwright.`,
108
+ });
109
+ }
110
+ }
111
+ else {
112
+ const server = detectWebServer(pkg, await exists(join(cwd, 'artisan')));
113
+ await write(cwd, CONFIG_PATH, playwrightConfig(server));
114
+ actions.push({
115
+ kind: 'created',
116
+ path: CONFIG_PATH,
117
+ message: server
118
+ ? `${CONFIG_PATH} dibuat (aplikasi dinyalakan dengan \`${server.command}\`)`
119
+ : `${CONFIG_PATH} dibuat — aplikasi tidak dikenali/Laravel: isi BASE_URL atau webServer agar test bisa membuka aplikasi`,
120
+ });
121
+ // 3. contoh test bila belum ada test e2e
122
+ const e2eFiles = await readdir(join(cwd, E2E_DIR)).catch(() => []);
123
+ if (!e2eFiles.some((f) => f.endsWith('.e2e.ts'))) {
124
+ await write(cwd, SMOKE_PATH, await readFile(SMOKE_TEMPLATE, 'utf8'));
125
+ actions.push({ kind: 'created', path: SMOKE_PATH, message: `${SMOKE_PATH} dibuat (contoh — ganti dengan alur penting aplikasi Anda)` });
126
+ }
127
+ }
128
+ // 4. Playwright eksplisit di package.json (npm 7+ memasang peer dependency, tetapi CI lebih aman eksplisit)
129
+ if (!deps['@playwright/test']) {
130
+ actions.push({ kind: 'hint', message: 'pasang Playwright: npm i -D @playwright/test' });
131
+ }
132
+ return actions;
25
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qaify-playwright-reporter",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Reporter Playwright untuk QA Factory (Qaify): kirim hasil test dan artefak ke /api/ingest, tanpa pernah menggagalkan test.",
5
5
  "license": "MIT",
6
6
  "author": "ahmdkenz",
@@ -23,8 +23,8 @@
23
23
  }
24
24
  },
25
25
  "bin": {
26
- "qaify": "./dist/cli.js",
27
- "playwright-reporter": "./dist/cli.js"
26
+ "qaify": "dist/cli.js",
27
+ "playwright-reporter": "dist/cli.js"
28
28
  },
29
29
  "files": [
30
30
  "dist",
package/templates/qa.yml CHANGED
@@ -28,7 +28,8 @@ jobs:
28
28
  - run: npx playwright install --with-deps
29
29
  # siapkan database seed/dummy di sini bila aplikasi butuh (jangan pernah database produksi)
30
30
  - run: npm run db:seed --if-present
31
- - run: npx playwright test
31
+ # hanya testDir di playwright.config (tests/e2e/*.e2e.ts); unit test Vitest/Jest jalankan di step terpisah bila perlu
32
+ - run: npx playwright test --pass-with-no-tests
32
33
 
33
34
  # Status final run & analisis kegagalan. Tetap jalan walau test gagal / di-cancel.
34
35
  # Analisis AI memakai langganan Claude Pro/Max Anda lewat Claude Code (QA Factory tidak memanggil AI):
@@ -0,0 +1,11 @@
1
+ import { expect, test } from '@playwright/test'
2
+
3
+ // Contoh dari `npx qaify init` — ganti/tambah dengan alur penting aplikasi Anda (login, checkout, dst.).
4
+ // File tests/e2e/*.e2e.ts dijalankan Playwright; unit test (*.test.ts) tetap milik Vitest/Jest.
5
+ test('halaman utama terbuka', async ({ page, baseURL }) => {
6
+ test.skip(!baseURL, 'Atur BASE_URL atau webServer di playwright.config.ts agar test ini membuka aplikasi')
7
+
8
+ const res = await page.goto('/')
9
+ expect(res?.status() ?? 0).toBeLessThan(400)
10
+ await expect(page.locator('body')).toBeVisible()
11
+ })