/** * ๐ŸŽ“ ่€ƒ่ฏ•็ป„็ป‡่€…่„šๆœฌ * * ๅœบๆ™ฏ: ๆˆ‘ๆ˜ฏ่€ƒ่ฏ•็ป„็ป‡่€… * 1. ๆทปๅŠ ่€ƒ็”Ÿไฟกๆฏ๏ผˆๅˆ›ๅปบ่€ƒ็”Ÿ่ดฆๅท๏ผ‰ * 2. ่€ƒ็”Ÿ่‡ช่กŒ็™ปๅฝ•็ณป็ปŸๅฎŒๆˆ่€ƒๆ ธ * 3. ๆŸฅ็œ‹่€ƒๆ ธ็ป“ๆžœ */ import { chromium } from 'playwright'; const API = 'http://localhost:3001'; const BASE = 'http://localhost:13001'; const TENANT_ID = 'a140a68e-f70a-44d3-b753-fa33d48cf234'; let pass = 0, fail = 0; function assert(label, ok, detail='') { if (ok) { pass++; console.log(` โœ… ${label}`); } else { fail++; console.log(` โŒ ${label}${detail?' โ€” '+detail:''}`); } } async function loginApi(u, p) { const r = await fetch(`${API}/api/auth/login`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:u,password:p})}); return r.ok ? (await r.json()).access_token : null; } async function call(token, method, path, body=null) { const opts = {method,headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json'}}; if(body)opts.body=JSON.stringify(body); const r = await fetch(`${API}/api${path}`,opts); return {status:r.status,data:await r.json().catch(()=>null)}; } /** Fill textarea via native setter + input event */ async function fillSA(page, text) { await page.waitForFunction(() => { const ta = document.querySelector('textarea'); return ta !== null && ta.offsetParent !== null; }, { timeout: 15000 }).catch(() => {}); // Double-check existence const exists = await page.evaluate(() => { const ta = document.querySelector('textarea'); return ta !== null && ta.offsetParent !== null; }); if (!exists) return false; await page.evaluate((t) => { const ta = document.querySelector('textarea'); if (!ta) return; Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set?.call(ta, t); ta.dispatchEvent(new Event('input', { bubbles: true })); }, text); await new Promise(r => setTimeout(r, 400)); return true; } /** Click send button */ async function clickSend(page) { await page.waitForFunction(() => { const btn = document.querySelector('button:has(svg.lucide-send)'); return btn && !btn.disabled; }, { timeout: 10000 }).catch(() => {}); await page.locator('button:has(svg.lucide-send)').last().click({ timeout: 5000 }).catch(() => { page.locator('button:has(svg.lucide-send)').last().click({ force: true, timeout: 3000 }).catch(() => {}); }); } /** Wait for spinner to clear */ async function waitIdle(page, ms = 1500) { await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 60000 }).catch(() => {}); await new Promise(r => setTimeout(r, ms)); } /** Extract last assistant message */ async function getQuestion(page) { return await page.evaluate(() => { const bubbles = Array.from(document.querySelectorAll('.px-5.py-4')); for (let i = bubbles.length - 1; i >= 0; i--) { const el = bubbles[i]; const text = el.textContent || ''; if (text.length > 25 && !(el.getAttribute('class') || '').includes('bg-indigo')) { return text.substring(0, 140).replace(/\s+/g, ' '); } } return ''; }); } /** Detect if assessment is done */ async function isDone(page) { const text = await page.textContent('body').catch(() => ''); return text.includes('ๅˆๆ ผ') || text.includes('VERIFIED') || text.includes('LEVEL:'); } // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // ACT 1: ๅˆ›ๅปบ่€ƒ็”Ÿ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ console.log('\n' + 'โ–ˆ'.repeat(70)); console.log(' ๐ŸŽ“ ๅœบๆ™ฏ: ่€ƒ่ฏ•็ป„็ป‡่€…ๆทปๅŠ ่€ƒ็”Ÿ'); console.log('โ–ˆ'.repeat(70)); const adminT = await loginApi('admin', 'admin123'); assert('็ป„็ป‡่€…็™ปๅฝ•', !!adminT); const CANDIDATES = [ { name: '่€ƒ็”Ÿๅฐๆ˜Ž', username: 'student1', password: 'exam123', level: 'ๅˆ็บง' }, { name: '่€ƒ็”Ÿๅฐ็บข', username: 'student2', password: 'exam123', level: 'ไธญ็บง' }, { name: '่€ƒ็”ŸๅฐๅŽ', username: 'student3', password: 'exam123', level: '้ซ˜็บง' }, { name: '่€ƒ็”ŸๅฐๆŽ', username: 'student4', password: 'exam123', level: 'ๅˆ็บง' }, ]; console.log('\nโ”€โ”€โ”€ 1. ๅˆ›ๅปบ่€ƒ็”Ÿ่ดฆๅท โ”€โ”€โ”€'); for (const s of CANDIDATES) { const r = await call(adminT, 'POST', '/users', { username: s.username, password: s.password, displayName: s.name, }); s.id = r.data?.user?.id || r.data?.id; assert(`ๅˆ›ๅปบ ${s.name}(${s.username})`, r.status === 200 || r.status === 201, `status=${r.status} id=${s.id?.substring(0,8)}`); if (s.id) { await call(adminT, 'POST', `/v1/tenants/${TENANT_ID}/members`, { userId: s.id, role: 'USER' }); } const t = await loginApi(s.username, s.password); assert(` ${s.name} ็™ปๅฝ•้ชŒ่ฏ`, !!t); } console.log('\n ่€ƒ็”Ÿๅฐฑ็ปช: ' + CANDIDATES.map(s => s.name).join('ใ€')); // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // ACT 2: ่€ƒ็”Ÿๅ‚ๅŠ ่€ƒๆ ธ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ console.log('\n' + 'โ–ˆ'.repeat(70)); console.log(' ๐Ÿ“ ๅœบๆ™ฏ: ่€ƒ็”Ÿๅ‚ๅŠ ่€ƒๆ ธ'); console.log('โ–ˆ'.repeat(70)); const browser = await chromium.launch({ headless: true }); const ANSWERS_POOL = { primary: [ 'ๆฃ€ๆŸฅไปฃ็ ๆœ‰ๆฒกๆœ‰bugๅ’Œ้”™่ฏฏ๏ผŒ็œ‹็œ‹่ƒฝไธ่ƒฝๆญฃๅธธ่ฟ่กŒใ€‚่ฟ˜่ฆๅ…ณๆณจๅฎ‰ๅ…จๆ€ง๏ผŒไธ่ƒฝๆœ‰ๆผๆดžใ€‚', '่ฟ˜่ฆ็œ‹ไปฃ็ ็š„ๆ€ง่ƒฝๅ’Œๅฏ่ฏปๆ€ง๏ผŒ่ฎฉๅˆซไบบไนŸ่ƒฝ็œ‹ๆ‡‚ใ€‚่ฆๆ€่€ƒAI็”Ÿๆˆ็š„ๅ†…ๅฎนๆ˜ฏๅฆๆญฃ็กฎใ€‚', '็”จๆธ…ๆ™ฐ็š„ๆ็คบ่ฏๅ‘Š่ฏ‰AIๅ…ทไฝ“่ฆๆฑ‚๏ผŒ็ป™ไพ‹ๅญ่ฏดๆ˜Žใ€‚ๅคๆ‚ไปปๅŠก่ฎฉAIไธ€ๆญฅไธ€ๆญฅๆ€่€ƒใ€‚', 'ไธ่ƒฝๆŠŠๆ•ๆ„Ÿไฟกๆฏ็ป™AI๏ผŒ่ฆๆณจๆ„ไฟๅฏ†ใ€‚AIๅชๆ˜ฏๅทฅๅ…ท๏ผŒๆœ€็ปˆ่ฆ่‡ชๅทฑๅšๅˆคๆ–ญใ€‚', ], mid: [ 'ไปฃ็ ๅฎกๆŸฅ่ฆๅ…ณๆณจๅŠŸ่ƒฝๆญฃ็กฎๆ€งใ€ๅฎ‰ๅ…จๆผๆดžใ€ๆ€ง่ƒฝ็“ถ้ขˆๅ’Œไปฃ็ ้ฃŽๆ ผไธ€่‡ดๆ€งใ€‚AI็”Ÿๆˆ็š„ไปฃ็ ้œ€่ฆไบบๅทฅ้ชŒ่ฏ้€ป่พ‘ๅฎŒๆ•ดๆ€งใ€‚', 'ๅ’ŒAIๅไฝœ่ฆๆ˜Ž็กฎๅˆ†ๅทฅ: AI่ดŸ่ดฃ็”Ÿๆˆๅ’Œๆ€ป็ป“๏ผŒไบบ่ดŸ่ดฃๅ†ณ็ญ–ๅ’Œ้ชŒ่ฏใ€‚ๅˆ†ๆญฅ้ชคๆฒŸ้€šๅฏไปฅๅ‡ๅฐ‘่ฏฏ่งฃใ€‚', 'ไผ˜ๅŒ–Prompt็š„ๅ…ณ้”ฎๆ˜ฏๅ…ทไฝ“ๅŒ–: ้™ๅฎš่Œƒๅ›ดใ€็ป™ๅ‡บ็คบไพ‹ใ€ๆ˜Ž็กฎ่พ“ๅ‡บๆ ผๅผใ€‚', 'AIๅฏ่ƒฝไบง็”Ÿๅนป่ง‰๏ผŒ่พ“ๅ‡บ็œ‹ไผผๅˆ็†ไฝ†ๅฎž้™…้”™่ฏฏ็š„ๅ†…ๅฎนใ€‚้œ€่ฆไบคๅ‰้ชŒ่ฏๅ…ณ้”ฎไบ‹ๅฎžใ€‚', ], advanced: [ 'ไปฃ็ ๅฎกๆŸฅ้œ€่ฆ็ณป็ปŸๆ€งๆฃ€ๆŸฅ: ๅŠŸ่ƒฝๅฎŒๆ•ดๆ€งใ€ๅฎ‰ๅ…จๆผๆดž(OWASP Top 10)ใ€ๆ€ง่ƒฝๅคๆ‚ๅบฆใ€ๅฏ็ปดๆŠคๆ€งใ€‚', 'AIๅไฝœ็š„ๆˆ็†Ÿๆจกๅผๆ˜ฏ"ไบบๅœจๅ›ž่ทฏไธญ": AIๅšๅฟซ้€ŸๅŽŸๅž‹ๅ’Œๆ‰น้‡ๅค„็†๏ผŒไบบๅšๆžถๆž„ๅ†ณ็ญ–ๅ’Œ่ดจ้‡ๆŠŠๅ…ณใ€‚', 'Prompt Engineering็š„ๆ ธๅฟƒ: ่ง’่‰ฒ่ฎพๅฎšใ€ไธŠไธ‹ๆ–‡้”šๅฎšใ€ๅˆ†ๆญฅๆŽจ็†(Chain-of-Thought)ใ€็บฆๆŸ่พน็•Œใ€‚', 'AIๅฎ‰ๅ…จไฝฟ็”จ: ่พ“ๅ…ฅ่พน็•Œ(ไธๆณ„้œฒๆ•ๆ„Ÿไฟกๆฏ)ใ€่พ“ๅ‡บ้ชŒ่ฏ(้˜ฒๆณจๅ…ฅๅ’Œๅนป่ง‰)ใ€ๆƒ้™ๆŽงๅˆถใ€‚', ], }; for (const s of CANDIDATES) { console.log(`\nโ”€โ”€โ”€ ${s.name}(${s.level}) ๅผ€ๅง‹่€ƒๆ ธ โ”€โ”€โ”€`); const levelKey = s.level === 'ๅˆ็บง' ? 'primary' : s.level === 'ไธญ็บง' ? 'mid' : 'advanced'; const answers = ANSWERS_POOL[levelKey]; let ansIdx = 0, saCount = 0, choiceCount = 0, followUpCount = 0; const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); try { // Login await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }); await page.waitForTimeout(1000); await page.locator('input[type="text"]').first().fill(s.username); await page.locator('input[type="password"]').first().fill(s.password); await page.locator('button[type="submit"]').click(); await page.waitForURL('**/'); // Enter assessment await page.goto(`${BASE}/assessment`, { waitUntil: 'networkidle' }); await page.waitForTimeout(2000); await page.locator('button:has-text("AIๅไฝœๆŠ€ๅทง")').first().click(); await page.waitForTimeout(500); await page.locator('button:has-text("ๅผ€ๅง‹ไธ“ไธš่ฏ„ไผฐ")').first().click(); // Wait for first question for (let i = 0; i < 120; i++) { const text = await page.textContent('body').catch(() => ''); if (text.includes('้—ฎ้ข˜ ') || text.includes('Question ')) break; await new Promise(r => setTimeout(r, 2000)); } await waitIdle(page); // Answer questions (up to 4) for (let q = 1; q <= 4; q++) { if (await isDone(page)) { console.log(' ๐Ÿ“‹ ่€ƒๆ ธๅทฒ็ป“ๆŸ'); break; } const qText = await getQuestion(page); console.log(` ็ฌฌ${q}/4้ข˜: ${qText || '(้ข˜ๅž‹ๆฃ€ๆต‹ไธญ)'}...`); // Wait for either choice or textarea for (let w = 0; w < 20; w++) { const t = await page.evaluate(() => { const opts = Array.from(document.querySelectorAll('button.w-full.text-left.px-5.py-4')) .filter(b => /^[A-D]/.test(b.textContent || '')); const ta = document.querySelector('textarea'); return { c: opts.length, sa: ta && ta.offsetParent !== null }; }); if (t.c > 0 || t.sa) break; await new Promise(r => setTimeout(r, 1500)); } if (await isDone(page)) break; // Detect final type const type = await page.evaluate(() => { const opts = Array.from(document.querySelectorAll('button.w-full.text-left.px-5.py-4')) .filter(b => /^[A-D]/.test(b.textContent || '')); const ta = document.querySelector('textarea'); return { isChoice: opts.length > 0, isSA: ta && ta.offsetParent !== null }; }); if (type.isChoice) { // โ”€โ”€ Choice โ”€โ”€ choiceCount++; const opts = page.locator('button.w-full.text-left.px-5.py-4'); const n = await opts.count(); if (n > 0) { await opts.nth(Math.min(1, n - 1)).click(); await new Promise(r => setTimeout(r, 300)); } const confirm = page.locator('button:has-text("็กฎ่ฎค็ญ”ๆกˆ")'); if (await confirm.isVisible().catch(() => false)) { await confirm.click(); } console.log(' ๐Ÿ“‹ ้€‰ๆ‹ฉ้ข˜ โ†’ ๅทฒ้€‰'); await waitIdle(page); } else if (type.isSA) { // โ”€โ”€ Short Answer โ”€โ”€ saCount++; const ans = answers[ansIdx % answers.length]; ansIdx++; const filled = await fillSA(page, ans); if (!filled) { // textarea disappeared; check if done if (await isDone(page)) break; q--; continue; } await clickSend(page); console.log(` โœ๏ธ ็ฎ€็ญ” โ†’ "${ans.substring(0, 28)}..."`); await waitIdle(page, 2000); // Check for follow-up const stillTA = await page.evaluate(() => { const ta = document.querySelector('textarea'); return ta && ta.offsetParent !== null; }); if (stillTA) { followUpCount++; const followAns = ans.includes('ๅฎ‰ๅ…จ') ? '่ฟ˜่ฆๆณจๆ„ๆƒ้™็ฎก็†ๅ’Œๅฎก่ฎกๆ—ฅๅฟ—่ฎฐๅฝ•ใ€‚' : '่ฟ˜ๆœ‰ๅฐฑๆ˜ฏ่ฆ่€ƒ่™‘ๅฏ็ปดๆŠคๆ€งๅ’Œๅ›ข้˜Ÿๅไฝœ่ง„่Œƒใ€‚'; await fillSA(page, followAns); await clickSend(page); console.log(' ๐Ÿ”„ ่ฟฝ้—ฎๅทฒ็ญ”'); await waitIdle(page); } } else { await new Promise(r => setTimeout(r, 2000)); q--; } } // Wait for results console.log(' โณ ็ญ‰ๅพ…่ฏ„ๅˆ†...'); await new Promise(r => setTimeout(r, 5000)); while (!(await isDone(page))) { await waitIdle(page, 3000); } const result = await page.evaluate(() => { const body = document.body.textContent || ''; const scores = Array.from(body.matchAll(/(\d+\.?\d*)\/10/g)).map(m => m[1]); const level = body.match(/LEVEL:\s*(\w+)/i)?.[1] || body.match(/็ญ‰็บง[๏ผš:]\s*(\w+)/)?.[1] || '?'; const passed = body.includes('ๅˆๆ ผ') || body.includes('VERIFIED'); return { scores: scores.join(', '), level, passed }; }); s.result = result; s.saCount = saCount; s.choiceCount = choiceCount; s.followUp = followUpCount; console.log(` ๐Ÿ“Š ${s.name}: ${result.passed ? '๐ŸŽ‰ ๅˆๆ ผ' : '๐Ÿ“ ๅฎŒๆˆ'} | ็ญ‰็บง=${result.level} | ๅพ—ๅˆ†=${result.scores || 'ๆ— '}`); } catch (err) { console.error(` โŒ ${s.name} ๅผ‚ๅธธ: ${err.message}`); s.error = err.message; s.result = { level: 'ERR', passed: false, scores: '' }; } finally { await page.close(); } } // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // ACT 3: ๆŸฅ็œ‹่€ƒๆ ธ็ป“ๆžœ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ console.log('\n' + 'โ–ˆ'.repeat(70)); console.log(' ๐Ÿ“Š ๅœบๆ™ฏ: ๆŸฅ็œ‹่€ƒๆ ธ็ป“ๆžœ'); console.log('โ–ˆ'.repeat(70)); console.log(''); console.log(' โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”'); console.log(' โ”‚ ๅ‡†่€ƒ่ฏๅท โ”‚ ๅง“ๅ โ”‚ ็บงๅˆซ โ”‚ ็ป“ๆžœ โ”‚ ็ญ‰็บง โ”‚ ๆ˜Ž็ป† โ”‚'); console.log(' โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค'); for (const s of CANDIDATES) { const st = s.result?.passed ? '๐ŸŽ‰ๅˆๆ ผ' : '๐Ÿ“ๅฎŒๆˆ'; const lv = s.result?.level || '?'; const dt = `${s.choiceCount||0}้€‰${s.saCount||0}็ฎ€${s.followUp||0}่ฟฝ`; console.log(` โ”‚ ${s.username.padEnd(8)} โ”‚ ${s.name.padEnd(6)} โ”‚ ${s.level.padEnd(4)} โ”‚ ${st.padEnd(5)} โ”‚ ${lv.padEnd(7)} โ”‚ ${dt.padEnd(16)} โ”‚`); } console.log(' โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜'); const passed = CANDIDATES.filter(s => s.result?.passed).length; console.log(`\n ๐Ÿ“ˆ ็ปŸ่ฎก: ่€ƒ็”Ÿ${CANDIDATES.length}ไบบ | ๅˆๆ ผ${passed}ไบบ | ไธๅˆๆ ผ${CANDIDATES.length-passed}ไบบ\n`); await browser.close(); console.log(` ๐ŸŽ“ ่€ƒ่ฏ•็ป„็ป‡ๅฎŒๆˆ: ${pass} โœ… / ${fail} โŒ`); if (fail > 0) process.exit(1);