File size: 13,134 Bytes
d44ff09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// ============================================================================
// REAL BROWSER E2E β€” Playwright + Chromium simulating an actual user.
// Covers all Section 2 screens, states, responsive, and network-failure.
// Captures real screenshots to ./screenshots/.
// ============================================================================
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');

const FRONTEND = 'http://localhost:5173';
const SHOTS = path.join(__dirname, 'screenshots');
fs.mkdirSync(SHOTS, { recursive: true });

const email = `realuser_${Date.now()}@example.com`;
const password = 'RealPass123!';
const licenseKey = 'ALK-DEMO-TEST-0001-0001';

const results = [];
function check(name, cond, detail) {
  const ok = !!cond;
  results.push({ name, ok, detail: detail || '' });
  console.log(`${ok ? 'βœ… PASS' : '❌ FAIL'}  ${name}${detail ? '  β€” ' + detail : ''}`);
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

(async () => {
  const browser = await chromium.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
  const ctx = await browser.newContext({ viewport: { width: 1366, height: 900 }, ignoreHTTPSErrors: true });
  const consoleErrors = [];
  const page = await ctx.newPage();
  page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(msg.text()); });
  page.on('requestfailed', (req) => { /* expected on network test */ });

  try {
    // ===== 1. LOGIN / REGISTER =====
    console.log('\n─── 1. LOGIN / REGISTER ───');
    await page.goto(FRONTEND + '/login', { waitUntil: 'networkidle' });
    await page.waitForSelector('input[type="email"]');
    await page.screenshot({ path: `${SHOTS}/01-login.png`, fullPage: true });

    await page.getByRole('button', { name: 'Register', exact: true }).click();
    await page.fill('input[type="email"]', email);
    await page.fill('input[type="password"]', 'short');
    await page.getByTestId('auth-submit').click();
    await sleep(400);
    const validationVisible = await page.locator('.error-text').count();
    check('FORM: inline validation shown for short password', validationVisible > 0, `error fields=${validationVisible}`);

    await page.fill('input[type="password"]', password);
    await page.screenshot({ path: `${SHOTS}/02-register-filled.png`, fullPage: true });
    // Submit + wait for navigation to dashboard
    await Promise.all([
      page.waitForURL('**/', { timeout: 15000 }),
      page.getByTestId('auth-submit').click(),
    ]);
    check('REGISTER: redirected to dashboard after signup', /\/$/.test(page.url()), page.url());

    // ===== 2. LICENSE ACTIVATION =====
    console.log('\n─── 2. LICENSE ACTIVATION ───');
    await page.goto(FRONTEND + '/account', { waitUntil: 'networkidle' });
    await page.waitForSelector('input[placeholder*="ALK"]');
    await page.fill('input[placeholder*="ALK"]', licenseKey);
    await Promise.all([
      page.waitForResponse((r) => r.url().includes('/activate-license'), { timeout: 10000 }).then((r) => r.status()),
      page.getByTestId('activate-btn').click(),
    ]).then(([s]) => console.log('   activate status:', s)).catch(() => console.log('   activate response wait issue'));
    await sleep(1200);
    const activeBadge = await page.locator('.badge').filter({ hasText: 'active' }).count();
    check('LICENSE: badge shows active after activation', activeBadge > 0);
    await page.screenshot({ path: `${SHOTS}/03-account-license-active.png`, fullPage: true });

    // ===== 3. DASHBOARD =====
    console.log('\n─── 3. DASHBOARD ───');
    await page.goto(FRONTEND + '/', { waitUntil: 'networkidle' });
    await page.waitForSelector('h1');
    const dashTitle = (await page.locator('h1').first().textContent()) || '';
    check('DASHBOARD: renders', dashTitle.toLowerCase().includes('dashboard'), `title="${dashTitle.trim()}"`);
    const emptyShown = await page.locator('.empty-state').count();
    check('DASHBOARD: empty state shown for new user', emptyShown > 0);
    await page.screenshot({ path: `${SHOTS}/04-dashboard-empty.png`, fullPage: true });

    // ===== 4. NEW CAMPAIGN =====
    console.log('\n─── 4. NEW CAMPAIGN ───');
    await page.goto(FRONTEND + '/campaigns/new', { waitUntil: 'networkidle' });
    await page.waitForSelector('#pn');
    const disabledBefore = await page.getByTestId('generate-btn').isDisabled();
    check('NEWCAMP: Generate disabled before valid input', disabledBefore);
    await page.fill('#pn', 'TrafficBlaster Pro');
    await page.fill('#pu', 'https://example.com/tb');
    await page.fill('#aud', 'beginner marketers');
    const disabledAfter = await page.getByTestId('generate-btn').isDisabled();
    check('NEWCAMP: Generate enabled after valid name', !disabledAfter);
    await page.screenshot({ path: `${SHOTS}/05-newcampaign-filled.png`, fullPage: true });

    await Promise.all([
      page.waitForURL(/\/campaigns\/[a-f0-9-]+$/, { timeout: 60000 }),
      page.getByTestId('generate-btn').click(),
    ]);
    const campaignId = page.url().split('/campaigns/')[1];
    check('NEWCAMP: navigated to editor after generate', !!campaignId, page.url());

    // ===== 5. EDITOR =====
    console.log('\n─── 5. EDITOR (5 tabs) ───');
    await page.waitForSelector('.tabs', { timeout: 15000 });
    await sleep(2000);
    await page.screenshot({ path: `${SHOTS}/06-editor-review.png`, fullPage: true });
    const reviewPreview = await page.locator('.preview-frame').count();
    check('EDITOR: Review tab renders HTML preview', reviewPreview > 0);

    for (const label of ['Bonus Page', 'Email Sequence', 'Social Posts', 'CTA']) {
      await page.getByRole('tab', { name: label }).click();
      await sleep(700);
    }
    check('EDITOR: all 4 other tabs clickable', true);

    // Email tab shows JSON preview
    await page.getByRole('tab', { name: 'Email Sequence' }).click();
    await sleep(700);
    const jsonPreview = await page.locator('.json-preview').count();
    check('EDITOR: Email tab shows JSON preview pane', jsonPreview > 0);
    await page.screenshot({ path: `${SHOTS}/07-editor-email-json.png`, fullPage: true });

    // Edit review -> dirty
    await page.getByRole('tab', { name: 'Review Page' }).click();
    await sleep(500);
    const ta = page.locator('textarea.code-editor').first();
    await ta.fill('<h1>Edited by real user via browser</h1><p>Live manual edit.</p>');
    await sleep(400);
    const unsavedBadge = await page.locator('.badge').filter({ hasText: 'Unsaved' }).count();
    check('EDITOR: unsaved-changes badge appears on edit', unsavedBadge > 0);

    // Save
    await page.getByTestId('save-btn').click();
    await sleep(1200);
    const savedToast = await page.locator('.toast.success').filter({ hasText: 'Saved' }).count();
    check('EDITOR: Save shows success toast', savedToast > 0);
    await page.screenshot({ path: `${SHOTS}/08-editor-saved.png`, fullPage: true });

    // Regenerate CTA
    await page.getByRole('tab', { name: 'CTA' }).click();
    await sleep(500);
    await page.fill('#ci', 'make them all High urgency');
    await page.getByTestId('regenerate-btn').click();
    await sleep(3000);
    const regenToast = await page.locator('.toast.success').filter({ hasText: 'Section updated' }).count();
    check('EDITOR: Regenerate single asset works', regenToast > 0);
    await page.screenshot({ path: `${SHOTS}/09-editor-regenerated.png`, fullPage: true });

    // ===== 6. EXPORT =====
    console.log('\n─── 6. EXPORT ───');
    await page.goto(FRONTEND + `/campaigns/${campaignId}/export`, { waitUntil: 'networkidle' });
    await page.waitForSelector('text=Export Formats');
    await page.screenshot({ path: `${SHOTS}/10-export.png`, fullPage: true });
    await page.getByTestId('export-btn').click();
    let exported = false;
    for (let i = 0; i < 40; i++) {
      await sleep(500);
      const ready = await page.locator('.toast.success').filter({ hasText: 'ready' }).count();
      if (ready > 0) { exported = true; break; }
    }
    check('EXPORT: packaging completes with success toast', exported);
    await sleep(800);
    const dlBtn = await page.locator('button').filter({ hasText: 'Download' }).count();
    check('EXPORT: download button appears in history', dlBtn > 0, `buttons=${dlBtn}`);
    await page.screenshot({ path: `${SHOTS}/11-export-done.png`, fullPage: true });

    // ===== 7. HISTORY =====
    console.log('\n─── 7. HISTORY ───');
    await page.goto(FRONTEND + '/history', { waitUntil: 'networkidle' });
    await page.waitForSelector('h1');
    await sleep(1000);
    const rows = await page.locator('table.tbl tbody tr').count();
    check('HISTORY: campaign row appears', rows >= 1, `rows=${rows}`);
    const statusBadge = (await page.locator('table.tbl .badge').first().textContent()) || '';
    check('HISTORY: status badge renders', !!statusBadge.trim(), `badge="${statusBadge.trim()}"`);
    await page.screenshot({ path: `${SHOTS}/12-history.png`, fullPage: true });

    // Search β†’ empty
    await page.fill('input[placeholder*="Search"]', 'NonExistentXYZ');
    await page.getByRole('button', { name: 'Search' }).click();
    await sleep(700);
    const emptyAfterSearch = await page.locator('.empty-state').count();
    check('HISTORY: search "no results" empty state works', emptyAfterSearch > 0);
    await page.screenshot({ path: `${SHOTS}/13-history-search-empty.png`, fullPage: true });

    // ===== 8. RESPONSIVE 375px =====
    console.log('\n─── 8. RESPONSIVE (375px) ───');
    await page.goto(FRONTEND + '/', { waitUntil: 'networkidle' });
    await page.setViewportSize({ width: 375, height: 812 });
    await sleep(700);
    const menuVisible = await page.locator('.menu-toggle').isVisible();
    check('RESPONSIVE: hamburger menu visible at 375px', menuVisible);
    await page.locator('.menu-toggle').click();
    await sleep(500);
    const sidebarOpen = await page.locator('.sidebar.open').count();
    check('RESPONSIVE: sidebar drawer opens on mobile', sidebarOpen > 0);
    await page.screenshot({ path: `${SHOTS}/14-mobile-dashboard.png`, fullPage: true });

    // Editor single column
    await page.goto(FRONTEND + `/campaigns/${campaignId}`, { waitUntil: 'networkidle' });
    await page.waitForSelector('.editor-wrap');
    await sleep(700);
    const cols = await page.evaluate(() => window.getComputedStyle(document.querySelector('.editor-wrap')).gridTemplateColumns);
    // Computed value resolves 1fr to px (e.g. "343px"); a single column = no space in the string.
    const isSingleColumn = cols.trim().indexOf(' ') === -1;
    check('RESPONSIVE: editor collapses to single column at 375px', isSingleColumn, `cols="${cols}"`);
    await page.screenshot({ path: `${SHOTS}/15-mobile-editor.png`, fullPage: true });

    // ===== 9. NETWORK FAILURE (scoped: only campaign listing fails, keep session) =====
    console.log('\n─── 9. NETWORK FAILURE ───');
    await page.setViewportSize({ width: 1366, height: 900 });
    await page.goto(FRONTEND + '/history', { waitUntil: 'networkidle' });
    await sleep(500);
    // Intercept only the campaigns list endpoint -> 500, leave refresh working so the
    // session stays valid and the History screen can render its error banner.
    await ctx.route('**/api/campaigns?**', (route) =>
      route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: { code: 'INTERNAL_ERROR', message: 'Failed to load campaigns' } }) }),
    );
    await page.reload({ waitUntil: 'domcontentloaded' });
    await sleep(1800);
    const errBanner = await page.locator('.error-banner').count();
    check('NETWORK-FAIL: error banner shown when API down', errBanner > 0, `banners=${errBanner}`);
    // Verify a Retry button is present (Section 2.5 Error state)
    const retryBtn = await page.locator('.error-banner button').count();
    check('NETWORK-FAIL: Retry button present in error state', retryBtn > 0, `retry buttons=${retryBtn}`);
    await page.screenshot({ path: `${SHOTS}/16-network-error.png`, fullPage: true });
    await ctx.unroute('**/api/campaigns?**');

    // ===== 10. CONSOLE =====
    console.log('\n─── 10. CONSOLE CLEANLINESS ───');
    const realErrors = consoleErrors.filter(
      (e) => !e.includes('Failed to load resource') && !/401|403/.test(e) && !e.includes('net::ERR'),
    );
    check('CONSOLE: no unexpected JS errors', realErrors.length === 0, `errors=${JSON.stringify(realErrors).slice(0, 200)}`);

  } catch (err) {
    console.error('\nπŸ’₯ HARNESS ERROR:', err.message);
    await page.screenshot({ path: `${SHOTS}/ERROR.png`, fullPage: true }).catch(() => {});
    check('HARNESS: completed without crashing', false, err.message);
  } finally {
    await browser.close();
  }

  const passed = results.filter((r) => r.ok).length;
  const total = results.length;
  console.log(`\n${'='.repeat(60)}\nREAL BROWSER E2E: ${passed}/${total} checks PASSED\n${'='.repeat(60)}`);
  process.exitCode = passed === total ? 0 : 1;
})();