Skip to content

Commit ebcfa73

Browse files
committed
feat(editor & capstone): add editor autocomplete navigation, pair deletion, case-sensitivity guard, and Final Project Capstone (v0.9.0)
1 parent 1840784 commit ebcfa73

41 files changed

Lines changed: 4181 additions & 123 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

API-Testing/course.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
// commented-out / fake snippet (e.g. `// expect(response.status()).toBe(200);`)
88
// can't satisfy a check meant for real, executed code.
99
function stripComments(code) {
10-
return code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
10+
const clean = code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
11+
if (/\.(GET|POST|PUT|DELETE|PATCH)\s*\(/.test(clean)) {
12+
const match = clean.match(/\.(GET|POST|PUT|DELETE|PATCH)\s*\(/)[1];
13+
throw new Error(`⚠️ ข้อผิดพลาด: Playwright และ JavaScript เป็น Case-sensitive กรุณาใช้ตัวพิมพ์เล็ก .${match.toLowerCase()}() เท่านั้น (ห้ามใช้ตัวพิมพ์ใหญ่ .${match})`);
14+
}
15+
return clean;
1116
}
1217

1318
const LESSONS = [

API-Testing/editor-autocomplete.js

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,60 @@ const EDITOR_WORD_MIN_LEN = 2;
88
var editorAutocompleteMatches = [];
99
var editorAutocompleteIndex = 0;
1010

11+
// Built-in domain keyword dictionary spanning all 12 course tracks:
12+
// API-Testing, Playwright, Robot-Framework, Performance-Testing, DB-Design-SQL,
13+
// CLI-Essentials, Security-Testing, Accessibility-Testing, Visual-Regression-Testing,
14+
// CI-CD-Pipeline, Framework-Design, Data-Structures-Algorithms.
15+
const BUILTIN_KEYWORDS = [
16+
// Playwright & API Methods
17+
'request', 'get', 'post', 'put', 'delete', 'patch', 'status', 'json', 'text', 'headers',
18+
'extraHTTPHeaders', 'newContext', 'dispose', 'page', 'goto', 'locator', 'getByRole',
19+
'getByText', 'getByTestId', 'getByPlaceholder', 'getByLabel', 'getByTitle', 'getByAltText',
20+
'click', 'fill', 'type', 'press', 'check', 'uncheck', 'selectOption', 'waitForSelector',
21+
'waitForURL', 'waitForLoadState', 'screenshot', 'pdf',
22+
23+
// Assertions & Matchers
24+
'expect', 'toBe', 'toEqual', 'toContain', 'toHaveProperty', 'toHaveLength', 'toBeOK',
25+
'toBeVisible', 'toHaveText', 'toContainText', 'toHaveValue', 'toBeChecked', 'toBeDisabled',
26+
'toBeEnabled', 'toHaveURL', 'toHaveTitle', 'toHaveCount', 'toHaveScreenshot', 'toBeDefined',
27+
'toBeUndefined', 'toBeTruthy', 'toBeFalsy',
28+
29+
// Robot Framework Keywords
30+
'Library', 'Browser', 'AppiumLibrary', 'SeleniumLibrary', 'Keywords', 'Variables',
31+
'New Page', 'Click', 'Fill Text', 'Get Text', 'Should Be Equal', 'Should Contain',
32+
'Should Be True', 'Open Browser', 'Input Text', 'Click Element', 'Element Should Be Visible',
33+
'Set Variable', 'Create List', 'Create Dictionary',
34+
35+
// k6 Performance Testing
36+
'http', 'del', 'group', 'sleep', 'options', 'vus', 'duration', 'stages', 'thresholds',
37+
'Trend', 'Counter', 'Gauge', 'Rate', 'scenario',
38+
39+
// SQL & DB Design
40+
'SELECT', 'FROM', 'WHERE', 'INSERT', 'INTO', 'UPDATE', 'SET', 'DELETE', 'JOIN', 'INNER',
41+
'LEFT', 'RIGHT', 'ON', 'GROUP', 'BY', 'HAVING', 'ORDER', 'ASC', 'DESC', 'LIMIT', 'OFFSET',
42+
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'PRIMARY', 'KEY', 'FOREIGN', 'UNIQUE', 'DEFAULT',
43+
'CREATE', 'TABLE', 'ALTER', 'DROP', 'INDEX', '1NF', '2NF', '3NF', 'BCNF',
44+
45+
// CLI, Git & Unix
46+
'git', 'commit', 'checkout', 'branch', 'push', 'pull', 'rebase', 'merge', 'log',
47+
'diff', 'clone', 'init', 'add', 'reset', 'stash', 'ls', 'cd', 'pwd', 'mkdir', 'rm',
48+
'cp', 'mv', 'cat', 'grep', 'find', 'chmod', 'chown', 'curl', 'ssh', 'tar', 'sed', 'awk',
49+
50+
// Security, A11y, Visual & CI/CD
51+
'XSS', 'SQLi', 'CSRF', 'sanitize', 'encodeURIComponent', 'innerHTML', 'textContent',
52+
'Authorization', 'Bearer', 'JWT', 'hash', 'salt', 'bcrypt', 'cors', 'rateLimit',
53+
'axe', 'violations', 'aria-label', 'aria-expanded', 'aria-hidden', 'role', 'tabindex',
54+
'alt', 'focus', 'blur', 'heading', 'button', 'link', 'img', 'contrast',
55+
'maxDiffPixels', 'maxDiffPixelRatio', 'threshold', 'mask', 'fullPage', 'clip',
56+
'pull_request', 'schedule', 'jobs', 'runs-on', 'ubuntu-latest', 'steps', 'uses', 'matrix',
57+
58+
// Framework Design & DSA
59+
'PageObject', 'BasePage', 'fixtures', 'extend', 'globalSetup', 'globalTeardown',
60+
'config', 'reporter', 'use', 'storageState', 'trace',
61+
'BigO', 'Array', 'Map', 'Set', 'Stack', 'Queue', 'LinkedList', 'BinaryTree', 'Graph',
62+
'DFS', 'BFS', 'binarySearch', 'quickSort', 'mergeSort', 'dynamicProgramming', 'memo', 'collision'
63+
];
64+
1165
// Word (identifier) touching the caret, e.g. typing "res" inside "const res" -> { start, end, word: "res" }
1266
function getCurrentWord(textarea) {
1367
const caret = textarea.selectionStart;
@@ -17,9 +71,10 @@ function getCurrentWord(textarea) {
1771
return { start, end: caret, word: value.slice(start, caret) };
1872
}
1973

20-
// Every distinct identifier already typed in the editor (word-based completion, like an IDE's local suggestions)
74+
// Every distinct identifier typed in the editor plus built-in domain keywords
2175
function getKnownWords(text) {
22-
return [...new Set(text.match(EDITOR_WORD_REGEX) || [])];
76+
const localWords = text.match(EDITOR_WORD_REGEX) || [];
77+
return [...new Set([...localWords, ...BUILTIN_KEYWORDS])];
2378
}
2479

2580
// Recompute autocomplete matches for the word at the caret and show/hide the dropdown accordingly
@@ -36,7 +91,7 @@ function updateEditorAutocomplete() {
3691
const known = getKnownWords(textarea.value);
3792
editorAutocompleteMatches = known
3893
.filter(w => w !== word && w.toLowerCase().startsWith(word.toLowerCase()))
39-
.slice(0, 6);
94+
.slice(0, 8);
4095

4196
if (!editorAutocompleteMatches.length) {
4297
hideEditorAutocomplete();
@@ -55,7 +110,7 @@ function getEditorAutocompleteEl(textarea) {
55110
if (!container) return null;
56111
el = document.createElement('div');
57112
el.id = 'editor-autocomplete';
58-
el.style.cssText = 'display: none; position: absolute; max-height: 160px; overflow-y: auto; background-color: #0b0f17; border: 1px solid var(--border-color); border-radius: 6px; z-index: 20; font-family: var(--font-mono); font-size: 0.8rem;';
113+
el.style.cssText = 'display: none; position: absolute; max-height: 180px; overflow-y: auto; background-color: #0b0f17; border: 1px solid var(--border-color); border-radius: 6px; z-index: 20; font-family: var(--font-mono); font-size: 0.8rem;';
59114
container.appendChild(el);
60115
}
61116
return el;

API-Testing/engine.js

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,63 @@ function handleAutoClosePair(textarea, key) {
8585
return false;
8686
}
8787

88+
// Handle deleting auto-closed pairs in tandem when backspacing between them
89+
function handlePairBackspace(textarea) {
90+
const start = textarea.selectionStart;
91+
const end = textarea.selectionEnd;
92+
if (start !== end || start === 0) return false;
93+
94+
const leftChar = textarea.value[start - 1];
95+
const rightChar = textarea.value[start];
96+
97+
if (AUTO_CLOSE_PAIRS[leftChar] && AUTO_CLOSE_PAIRS[leftChar] === rightChar) {
98+
textarea.value = textarea.value.slice(0, start - 1) + textarea.value.slice(start + 1);
99+
textarea.selectionStart = textarea.selectionEnd = start - 1;
100+
updateGutter();
101+
if (typeof updateEditorAutocomplete === 'function') updateEditorAutocomplete();
102+
return true;
103+
}
104+
return false;
105+
}
106+
107+
// Handle smart Enter inside bracket pairs ({}, (), []) with proper indentation
108+
function handleSmartEnter(textarea) {
109+
const start = textarea.selectionStart;
110+
const end = textarea.selectionEnd;
111+
if (start !== end) return false;
112+
113+
const val = textarea.value;
114+
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
115+
const currentLine = val.slice(lineStart, start);
116+
const baseIndent = (currentLine.match(/^\s*/) || [''])[0];
117+
118+
const leftChar = val[start - 1];
119+
const rightChar = val[start];
120+
121+
if (leftChar && AUTO_CLOSE_PAIRS[leftChar] && AUTO_CLOSE_PAIRS[leftChar] === rightChar) {
122+
const tabIndent = ' '.repeat(typeof TAB_WIDTH === 'number' ? TAB_WIDTH : 2);
123+
const innerIndent = baseIndent + tabIndent;
124+
const insertText = '\n' + innerIndent + '\n' + baseIndent;
125+
126+
textarea.value = val.slice(0, start) + insertText + val.slice(start);
127+
textarea.selectionStart = textarea.selectionEnd = start + 1 + innerIndent.length;
128+
updateGutter();
129+
if (typeof updateEditorAutocomplete === 'function') updateEditorAutocomplete();
130+
return true;
131+
}
132+
133+
if (baseIndent.length > 0) {
134+
const insertText = '\n' + baseIndent;
135+
textarea.value = val.slice(0, start) + insertText + val.slice(start);
136+
textarea.selectionStart = textarea.selectionEnd = start + insertText.length;
137+
updateGutter();
138+
if (typeof updateEditorAutocomplete === 'function') updateEditorAutocomplete();
139+
return true;
140+
}
141+
142+
return false;
143+
}
144+
88145
// Keydown handler to prevent tab key escaping the editor
89146
function handleTextareaKeydown(e) {
90147
const textarea = e.target;
@@ -93,6 +150,13 @@ function handleTextareaKeydown(e) {
93150
if (handleEditorAutocompleteKeydown(e)) return;
94151
}
95152

153+
if (e.key === 'Backspace') {
154+
if (handlePairBackspace(textarea)) {
155+
e.preventDefault();
156+
return;
157+
}
158+
}
159+
96160
if (AUTO_CLOSE_PAIRS[e.key] || Object.values(AUTO_CLOSE_PAIRS).includes(e.key)) {
97161
if (handleAutoClosePair(textarea, e.key)) {
98162
e.preventDefault();
@@ -120,10 +184,17 @@ function handleTextareaKeydown(e) {
120184
hideEditorAutocomplete();
121185
}
122186

123-
// CMD/Ctrl + Enter shortcut to run tests
124-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
125-
e.preventDefault();
126-
runSandboxCode();
187+
if (e.key === 'Enter') {
188+
if (e.metaKey || e.ctrlKey) {
189+
e.preventDefault();
190+
runSandboxCode();
191+
return;
192+
}
193+
194+
if (handleSmartEnter(textarea)) {
195+
e.preventDefault();
196+
return;
197+
}
127198
}
128199
}
129200

Accessibility-Testing/editor-autocomplete.js

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,60 @@ const EDITOR_WORD_MIN_LEN = 2;
88
var editorAutocompleteMatches = [];
99
var editorAutocompleteIndex = 0;
1010

11+
// Built-in domain keyword dictionary spanning all 12 course tracks:
12+
// API-Testing, Playwright, Robot-Framework, Performance-Testing, DB-Design-SQL,
13+
// CLI-Essentials, Security-Testing, Accessibility-Testing, Visual-Regression-Testing,
14+
// CI-CD-Pipeline, Framework-Design, Data-Structures-Algorithms.
15+
const BUILTIN_KEYWORDS = [
16+
// Playwright & API Methods
17+
'request', 'get', 'post', 'put', 'delete', 'patch', 'status', 'json', 'text', 'headers',
18+
'extraHTTPHeaders', 'newContext', 'dispose', 'page', 'goto', 'locator', 'getByRole',
19+
'getByText', 'getByTestId', 'getByPlaceholder', 'getByLabel', 'getByTitle', 'getByAltText',
20+
'click', 'fill', 'type', 'press', 'check', 'uncheck', 'selectOption', 'waitForSelector',
21+
'waitForURL', 'waitForLoadState', 'screenshot', 'pdf',
22+
23+
// Assertions & Matchers
24+
'expect', 'toBe', 'toEqual', 'toContain', 'toHaveProperty', 'toHaveLength', 'toBeOK',
25+
'toBeVisible', 'toHaveText', 'toContainText', 'toHaveValue', 'toBeChecked', 'toBeDisabled',
26+
'toBeEnabled', 'toHaveURL', 'toHaveTitle', 'toHaveCount', 'toHaveScreenshot', 'toBeDefined',
27+
'toBeUndefined', 'toBeTruthy', 'toBeFalsy',
28+
29+
// Robot Framework Keywords
30+
'Library', 'Browser', 'AppiumLibrary', 'SeleniumLibrary', 'Keywords', 'Variables',
31+
'New Page', 'Click', 'Fill Text', 'Get Text', 'Should Be Equal', 'Should Contain',
32+
'Should Be True', 'Open Browser', 'Input Text', 'Click Element', 'Element Should Be Visible',
33+
'Set Variable', 'Create List', 'Create Dictionary',
34+
35+
// k6 Performance Testing
36+
'http', 'del', 'group', 'sleep', 'options', 'vus', 'duration', 'stages', 'thresholds',
37+
'Trend', 'Counter', 'Gauge', 'Rate', 'scenario',
38+
39+
// SQL & DB Design
40+
'SELECT', 'FROM', 'WHERE', 'INSERT', 'INTO', 'UPDATE', 'SET', 'DELETE', 'JOIN', 'INNER',
41+
'LEFT', 'RIGHT', 'ON', 'GROUP', 'BY', 'HAVING', 'ORDER', 'ASC', 'DESC', 'LIMIT', 'OFFSET',
42+
'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'PRIMARY', 'KEY', 'FOREIGN', 'UNIQUE', 'DEFAULT',
43+
'CREATE', 'TABLE', 'ALTER', 'DROP', 'INDEX', '1NF', '2NF', '3NF', 'BCNF',
44+
45+
// CLI, Git & Unix
46+
'git', 'commit', 'checkout', 'branch', 'push', 'pull', 'rebase', 'merge', 'log',
47+
'diff', 'clone', 'init', 'add', 'reset', 'stash', 'ls', 'cd', 'pwd', 'mkdir', 'rm',
48+
'cp', 'mv', 'cat', 'grep', 'find', 'chmod', 'chown', 'curl', 'ssh', 'tar', 'sed', 'awk',
49+
50+
// Security, A11y, Visual & CI/CD
51+
'XSS', 'SQLi', 'CSRF', 'sanitize', 'encodeURIComponent', 'innerHTML', 'textContent',
52+
'Authorization', 'Bearer', 'JWT', 'hash', 'salt', 'bcrypt', 'cors', 'rateLimit',
53+
'axe', 'violations', 'aria-label', 'aria-expanded', 'aria-hidden', 'role', 'tabindex',
54+
'alt', 'focus', 'blur', 'heading', 'button', 'link', 'img', 'contrast',
55+
'maxDiffPixels', 'maxDiffPixelRatio', 'threshold', 'mask', 'fullPage', 'clip',
56+
'pull_request', 'schedule', 'jobs', 'runs-on', 'ubuntu-latest', 'steps', 'uses', 'matrix',
57+
58+
// Framework Design & DSA
59+
'PageObject', 'BasePage', 'fixtures', 'extend', 'globalSetup', 'globalTeardown',
60+
'config', 'reporter', 'use', 'storageState', 'trace',
61+
'BigO', 'Array', 'Map', 'Set', 'Stack', 'Queue', 'LinkedList', 'BinaryTree', 'Graph',
62+
'DFS', 'BFS', 'binarySearch', 'quickSort', 'mergeSort', 'dynamicProgramming', 'memo', 'collision'
63+
];
64+
1165
// Word (identifier) touching the caret, e.g. typing "res" inside "const res" -> { start, end, word: "res" }
1266
function getCurrentWord(textarea) {
1367
const caret = textarea.selectionStart;
@@ -17,9 +71,10 @@ function getCurrentWord(textarea) {
1771
return { start, end: caret, word: value.slice(start, caret) };
1872
}
1973

20-
// Every distinct identifier already typed in the editor (word-based completion, like an IDE's local suggestions)
74+
// Every distinct identifier typed in the editor plus built-in domain keywords
2175
function getKnownWords(text) {
22-
return [...new Set(text.match(EDITOR_WORD_REGEX) || [])];
76+
const localWords = text.match(EDITOR_WORD_REGEX) || [];
77+
return [...new Set([...localWords, ...BUILTIN_KEYWORDS])];
2378
}
2479

2580
// Recompute autocomplete matches for the word at the caret and show/hide the dropdown accordingly
@@ -36,7 +91,7 @@ function updateEditorAutocomplete() {
3691
const known = getKnownWords(textarea.value);
3792
editorAutocompleteMatches = known
3893
.filter(w => w !== word && w.toLowerCase().startsWith(word.toLowerCase()))
39-
.slice(0, 6);
94+
.slice(0, 8);
4095

4196
if (!editorAutocompleteMatches.length) {
4297
hideEditorAutocomplete();
@@ -55,7 +110,7 @@ function getEditorAutocompleteEl(textarea) {
55110
if (!container) return null;
56111
el = document.createElement('div');
57112
el.id = 'editor-autocomplete';
58-
el.style.cssText = 'display: none; position: absolute; max-height: 160px; overflow-y: auto; background-color: #0b0f17; border: 1px solid var(--border-color); border-radius: 6px; z-index: 20; font-family: var(--font-mono); font-size: 0.8rem;';
113+
el.style.cssText = 'display: none; position: absolute; max-height: 180px; overflow-y: auto; background-color: #0b0f17; border: 1px solid var(--border-color); border-radius: 6px; z-index: 20; font-family: var(--font-mono); font-size: 0.8rem;';
59114
container.appendChild(el);
60115
}
61116
return el;

Accessibility-Testing/engine.js

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,63 @@ function handleAutoClosePair(textarea, key) {
8585
return false;
8686
}
8787

88+
// Handle deleting auto-closed pairs in tandem when backspacing between them
89+
function handlePairBackspace(textarea) {
90+
const start = textarea.selectionStart;
91+
const end = textarea.selectionEnd;
92+
if (start !== end || start === 0) return false;
93+
94+
const leftChar = textarea.value[start - 1];
95+
const rightChar = textarea.value[start];
96+
97+
if (AUTO_CLOSE_PAIRS[leftChar] && AUTO_CLOSE_PAIRS[leftChar] === rightChar) {
98+
textarea.value = textarea.value.slice(0, start - 1) + textarea.value.slice(start + 1);
99+
textarea.selectionStart = textarea.selectionEnd = start - 1;
100+
updateGutter();
101+
if (typeof updateEditorAutocomplete === 'function') updateEditorAutocomplete();
102+
return true;
103+
}
104+
return false;
105+
}
106+
107+
// Handle smart Enter inside bracket pairs ({}, (), []) with proper indentation
108+
function handleSmartEnter(textarea) {
109+
const start = textarea.selectionStart;
110+
const end = textarea.selectionEnd;
111+
if (start !== end) return false;
112+
113+
const val = textarea.value;
114+
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
115+
const currentLine = val.slice(lineStart, start);
116+
const baseIndent = (currentLine.match(/^\s*/) || [''])[0];
117+
118+
const leftChar = val[start - 1];
119+
const rightChar = val[start];
120+
121+
if (leftChar && AUTO_CLOSE_PAIRS[leftChar] && AUTO_CLOSE_PAIRS[leftChar] === rightChar) {
122+
const tabIndent = ' '.repeat(typeof TAB_WIDTH === 'number' ? TAB_WIDTH : 2);
123+
const innerIndent = baseIndent + tabIndent;
124+
const insertText = '\n' + innerIndent + '\n' + baseIndent;
125+
126+
textarea.value = val.slice(0, start) + insertText + val.slice(start);
127+
textarea.selectionStart = textarea.selectionEnd = start + 1 + innerIndent.length;
128+
updateGutter();
129+
if (typeof updateEditorAutocomplete === 'function') updateEditorAutocomplete();
130+
return true;
131+
}
132+
133+
if (baseIndent.length > 0) {
134+
const insertText = '\n' + baseIndent;
135+
textarea.value = val.slice(0, start) + insertText + val.slice(start);
136+
textarea.selectionStart = textarea.selectionEnd = start + insertText.length;
137+
updateGutter();
138+
if (typeof updateEditorAutocomplete === 'function') updateEditorAutocomplete();
139+
return true;
140+
}
141+
142+
return false;
143+
}
144+
88145
// Keydown handler to prevent tab key escaping the editor
89146
function handleTextareaKeydown(e) {
90147
const textarea = e.target;
@@ -93,6 +150,13 @@ function handleTextareaKeydown(e) {
93150
if (handleEditorAutocompleteKeydown(e)) return;
94151
}
95152

153+
if (e.key === 'Backspace') {
154+
if (handlePairBackspace(textarea)) {
155+
e.preventDefault();
156+
return;
157+
}
158+
}
159+
96160
if (AUTO_CLOSE_PAIRS[e.key] || Object.values(AUTO_CLOSE_PAIRS).includes(e.key)) {
97161
if (handleAutoClosePair(textarea, e.key)) {
98162
e.preventDefault();
@@ -120,10 +184,17 @@ function handleTextareaKeydown(e) {
120184
hideEditorAutocomplete();
121185
}
122186

123-
// CMD/Ctrl + Enter shortcut to run tests
124-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
125-
e.preventDefault();
126-
runSandboxCode();
187+
if (e.key === 'Enter') {
188+
if (e.metaKey || e.ctrlKey) {
189+
e.preventDefault();
190+
runSandboxCode();
191+
return;
192+
}
193+
194+
if (handleSmartEnter(textarea)) {
195+
e.preventDefault();
196+
return;
197+
}
127198
}
128199
}
129200

0 commit comments

Comments
 (0)