diff --git a/.github/workflows/lint-code-blocks.yml b/.github/workflows/lint-code-blocks.yml
new file mode 100644
index 000000000..3022d8c23
--- /dev/null
+++ b/.github/workflows/lint-code-blocks.yml
@@ -0,0 +1,53 @@
+name: Lint code blocks
+
+# Syntax-checks fenced code blocks in docs changed by this PR.
+#
+# Scoped to CHANGED FILES on purpose: the repo has pre-existing debt, and a
+# repo-wide gate would block every PR on unrelated pages. This stops new
+# breakage landing while the backlog is worked down. To sweep everything:
+# node scripts/lint-code-blocks.js --all
+
+on:
+ pull_request:
+ paths:
+ - 'docs/**/*.md'
+ - 'docs/**/*.mdx'
+ - 'scripts/lint-code-blocks.js'
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install PyYAML
+ run: pip install --quiet pyyaml
+
+ # ruby and bash are already present on ubuntu-latest
+
+ - name: Collect changed docs
+ id: changed
+ run: |
+ git diff --name-only --diff-filter=ACMR \
+ "origin/${{ github.base_ref }}" HEAD -- 'docs/**/*.md' 'docs/**/*.mdx' \
+ > changed.txt || true
+ echo "count=$(wc -l < changed.txt | tr -d ' ')" >> "$GITHUB_OUTPUT"
+ echo "Changed doc files:"; cat changed.txt
+
+ - name: Lint fenced code blocks
+ if: steps.changed.outputs.count != '0'
+ run: xargs -a changed.txt node scripts/lint-code-blocks.js
+
+ - name: Nothing to lint
+ if: steps.changed.outputs.count == '0'
+ run: echo "No documentation files changed."
diff --git a/docs/smartui-appium-java-sdk.md b/docs/smartui-appium-java-sdk.md
index 1bbbed7f3..4d27729ad 100644
--- a/docs/smartui-appium-java-sdk.md
+++ b/docs/smartui-appium-java-sdk.md
@@ -105,14 +105,14 @@ export PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```bash
-set PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+set PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```powershell
-$env:PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+$env:PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
diff --git a/docs/smartui-cypress-sdk.md b/docs/smartui-cypress-sdk.md
index 6f1e6b005..7e6026048 100644
--- a/docs/smartui-cypress-sdk.md
+++ b/docs/smartui-cypress-sdk.md
@@ -135,14 +135,14 @@ export PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```bash
-set PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+set PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```powershell
-$env:PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+$env:PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
@@ -586,7 +586,7 @@ cy.smartuiSnapshot('Page Loaded');
2. Check configuration file syntax
3. Try different port if default is in use:
```bash
- npx smartui exec -P 5000 --
+ npx smartui exec -P 5000 --
```
4. Check file permissions for configuration and project files
diff --git a/docs/smartui-k6-setup.md b/docs/smartui-k6-setup.md
index 0a118d065..70017c502 100644
--- a/docs/smartui-k6-setup.md
+++ b/docs/smartui-k6-setup.md
@@ -51,14 +51,14 @@ export LT_USERNAME="YOUR_USERNAME"
```bash
-set LT_USERNAME=YOUR_USERNAME"
+set LT_USERNAME="YOUR_USERNAME"
```
```powershell
-$env:LT_USERNAME=YOUR_USERNAME"
+$env:LT_USERNAME="YOUR_USERNAME"
```
diff --git a/docs/smartui-playwright-java-sdk.md b/docs/smartui-playwright-java-sdk.md
index e5c5bbb29..c2da37884 100644
--- a/docs/smartui-playwright-java-sdk.md
+++ b/docs/smartui-playwright-java-sdk.md
@@ -599,7 +599,7 @@ SmartUISnapshot.smartuiSnapshot(driver, "Page Loaded");
2. Check configuration file syntax
3. Try different port if default is in use:
```bash
- npx smartui exec -P 5000 --
+ npx smartui exec -P 5000 --
```
4. Check file permissions for configuration and project files
diff --git a/docs/smartui-playwright-sdk.md b/docs/smartui-playwright-sdk.md
index 19f30297d..6ae7bb33c 100644
--- a/docs/smartui-playwright-sdk.md
+++ b/docs/smartui-playwright-sdk.md
@@ -105,21 +105,21 @@ Setup your project token shown in the **SmartUI** app after creating your projec
```bash
-export PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+export PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```bash
-set PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+set PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```powershell
-$env:PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+$env:PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
@@ -631,7 +631,7 @@ await smartuiSnapshot.smartuiSnapshot(page, Page Loaded");
2. Check configuration file syntax
3. Try different port if default is in use:
```bash
- npx smartui exec -P 5000 --
+ npx smartui exec -P 5000 --
```
4. Check file permissions for configuration and project files
diff --git a/docs/smartui-puppeteer-sdk.md b/docs/smartui-puppeteer-sdk.md
index fd64bcab2..944491c81 100644
--- a/docs/smartui-puppeteer-sdk.md
+++ b/docs/smartui-puppeteer-sdk.md
@@ -107,21 +107,21 @@ Setup your project token shown in the **SmartUI** app after creating your projec
```bash
-export PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+export PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```bash
-set PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+set PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```powershell
-$env:PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+$env:PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
@@ -636,7 +636,7 @@ await smartuiSnapshot(page, Page Loaded");
2. Check configuration file syntax
3. Try different port if default is in use:
```bash
- npx smartui exec -P 5000 --
+ npx smartui exec -P 5000 --
```
4. Check file permissions for configuration and project files
diff --git a/docs/smartui-selenium-java-sdk.md b/docs/smartui-selenium-java-sdk.md
index 2c2250406..068276d78 100644
--- a/docs/smartui-selenium-java-sdk.md
+++ b/docs/smartui-selenium-java-sdk.md
@@ -140,7 +140,7 @@ set PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```powershell
-$env:PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+$env:PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
@@ -658,7 +658,7 @@ SmartUISnapshot.smartuiSnapshot(driver, "Page Loaded");
2. Check configuration file syntax
3. Try different port if default is in use:
```bash
- npx smartui exec -P 5000 --
+ npx smartui exec -P 5000 --
```
4. Check file permissions for configuration and project files
diff --git a/docs/smartui-selenium-js-sdk.md b/docs/smartui-selenium-js-sdk.md
index 967862c81..46553d557 100644
--- a/docs/smartui-selenium-js-sdk.md
+++ b/docs/smartui-selenium-js-sdk.md
@@ -107,21 +107,21 @@ Setup your project token shown in the **SmartUI** app after creating your projec
```bash
-export PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+export PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```bash
-set PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+set PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```powershell
-$env:PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+$env:PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
@@ -624,7 +624,7 @@ await smartuiSnapshot(driver, Page Loaded");
2. Check configuration file syntax
3. Try different port if default is in use:
```bash
- npx smartui exec -P 5000 --
+ npx smartui exec -P 5000 --
```
4. Check file permissions for configuration and project files
diff --git a/docs/smartui-selenium-ruby-sdk.md b/docs/smartui-selenium-ruby-sdk.md
index d6dce6c64..69ed8cc86 100644
--- a/docs/smartui-selenium-ruby-sdk.md
+++ b/docs/smartui-selenium-ruby-sdk.md
@@ -111,21 +111,21 @@ Setup your project token shown in the **SmartUI** app after creating your projec
```bash
-export PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+export PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```bash
-set PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+set PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```powershell
-$env:PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+$env:PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
@@ -613,7 +613,7 @@ LambdaTest::Selenium::Driver.smartui_snapshot(driver, Page Loaded")
2. Check configuration file syntax
3. Try different port if default is in use:
```bash
- npx smartui exec -P 5000 --
+ npx smartui exec -P 5000 --
```
4. Check file permissions for configuration and project files
diff --git a/docs/smartui-testcafe-sdk.md b/docs/smartui-testcafe-sdk.md
index 90d574473..5f716f6fe 100644
--- a/docs/smartui-testcafe-sdk.md
+++ b/docs/smartui-testcafe-sdk.md
@@ -114,14 +114,14 @@ export PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```bash
-set PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+set PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```powershell
-$env:PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+$env:PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
@@ -595,7 +595,7 @@ test('Take screenshot after page loads', async t ="> {
2. Check configuration file syntax
3. Try different port if default is in use:
```bash
- npx smartui exec -P 5000 --
+ npx smartui exec -P 5000 --
```
4. Check file permissions for configuration and project files
diff --git a/docs/smartui-wdio-sdk.md b/docs/smartui-wdio-sdk.md
index df25d62d9..2b78dac78 100644
--- a/docs/smartui-wdio-sdk.md
+++ b/docs/smartui-wdio-sdk.md
@@ -117,14 +117,14 @@ export PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```bash
-set PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+set PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
```powershell
-$env:PROJECT_TOKEN=123456#1234abcd-****-****-****-************"
+$env:PROJECT_TOKEN="123456#1234abcd-****-****-****-************"
```
@@ -568,7 +568,7 @@ await smartuiSnapshot(driver, 'Page Loaded');
2. Check configuration file syntax
3. Try different port if default is in use:
```bash
- npx smartui exec -P 5000 --
+ npx smartui exec -P 5000 --
```
4. Check file permissions for configuration and project files
diff --git a/docs/smartui-with-semaphore.md b/docs/smartui-with-semaphore.md
index b0c4f4bd7..9c14e81f8 100644
--- a/docs/smartui-with-semaphore.md
+++ b/docs/smartui-with-semaphore.md
@@ -265,13 +265,13 @@ env_vars:
jobs:
- name: Run Tests
commands:
- - npx smartui exec --
+ - npx smartui exec --
- name: Test Group 2
task:
jobs:
- name: Run Tests
commands:
- - npx smartui exec --
+ - npx smartui exec --
```
3. Optimize test execution
diff --git a/scripts/lint-code-blocks.js b/scripts/lint-code-blocks.js
new file mode 100755
index 000000000..bd58d4006
--- /dev/null
+++ b/scripts/lint-code-blocks.js
@@ -0,0 +1,156 @@
+#!/usr/bin/env node
+/**
+ * Syntax-check fenced code blocks in Markdown/MDX docs.
+ *
+ * Why this exists: an audit of the SmartUI doc set found 118 code blocks that
+ * could not parse or compile - missing quotes, JavaScript object literals inside
+ * Python blocks, unclosed fences, curly quotes. Every one would have been caught
+ * here on the commit that introduced it.
+ *
+ * Usage:
+ * node scripts/lint-code-blocks.js lint specific files
+ * node scripts/lint-code-blocks.js --all lint every doc
+ *
+ * Exit code 1 if any block fails.
+ */
+'use strict';
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFileSync, spawnSync } = require('child_process');
+
+const args = process.argv.slice(2);
+const ALL = args.includes('--all');
+let files = args.filter((a) => !a.startsWith('--'));
+
+if (ALL) {
+ const walk = (dir) =>
+ fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) return walk(p);
+ return /\.mdx?$/.test(e.name) ? [p] : [];
+ });
+ files = ['docs'].filter(fs.existsSync).flatMap(walk);
+}
+files = files.filter((f) => /\.mdx?$/.test(f) && fs.existsSync(f));
+if (!files.length) {
+ console.log('lint-code-blocks: no markdown files to check');
+ process.exit(0);
+}
+
+// ── which languages we can actually validate, and how ────────────────────────
+const NORMALISE = {
+ js: 'javascript', node: 'javascript', nodejs: 'javascript',
+ py: 'python', rb: 'ruby', cs: 'csharp', sh: 'bash', shell: 'bash', zsh: 'bash',
+};
+const have = (cmd) => spawnSync(cmd, ['--version'], { stdio: 'ignore' }).status === 0;
+const HAVE = { node: true, python3: have('python3'), ruby: have('ruby'), bash: have('bash') };
+
+// Markdown nests blocks inside lists and s, so the content arrives
+// uniformly indented. Strip the common leading whitespace before parsing -
+// otherwise every indented Python block is a false IndentationError.
+function dedent(src) {
+ const ls = src.split('\n').filter((l) => l.trim());
+ if (!ls.length) return src;
+ const pad = Math.min(...ls.map((l) => l.match(/^[ \t]*/)[0].length));
+ return pad ? src.split('\n').map((l) => l.slice(pad)).join('\n') : src;
+}
+
+function tmp(content, ext) {
+ const f = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'blk-')), 'block' + ext);
+ fs.writeFileSync(f, content);
+ return f;
+}
+function run(cmd, argv) {
+ const r = spawnSync(cmd, argv, { encoding: 'utf8' });
+ return { ok: r.status === 0, err: ((r.stderr || '') + (r.stdout || '')).trim().split('\n')[0] };
+}
+
+const CHECKERS = {
+ javascript: (c) => (HAVE.node ? run('node', ['--check', tmp(c, '.mjs')]) : null),
+ python: (c) => (HAVE.python3 ? run('python3', ['-c', 'import ast,sys;ast.parse(open(sys.argv[1]).read())', tmp(c, '.py')]) : null),
+ ruby: (c) => (HAVE.ruby ? run('ruby', ['-c', tmp(c, '.rb')]) : null),
+ bash: (c) => (HAVE.bash ? run('bash', ['-n', tmp(c, '.sh')]) : null),
+ json: (c) => { try { JSON.parse(c); return { ok: true }; } catch (e) { return { ok: false, err: e.message }; } },
+ yaml: (c) => (HAVE.python3 ? run('python3', ['-c', 'import yaml,sys;list(yaml.safe_load_all(open(sys.argv[1])))', tmp(c, '.yml')]) : null),
+};
+
+// Blocks that are deliberately partial. A bare object/array fragment or an
+// elision marker is legitimate in docs and must not fail the build.
+function isFragment(code, lang) {
+ const t = code.trim();
+ if (/^\s*(\.\.\.|\/\/\s*\.\.\.|#\s*\.\.\.)/m.test(t)) return true;
+ if (lang === 'javascript' && /^['"][^'"]+['"]\s*:/.test(t)) return true; // 'LT:Options': { ... }
+ if (lang === 'javascript' && /^\{[\s\S]*\}$/.test(t) && !/[;=]/.test(t)) return true;
+ if (lang === 'json' && /^\s*"[^"]+"\s*:/.test(t) && !t.startsWith('{')) return true;
+ if (/^[\w.$-]+\s*:\s*[{[]/.test(t)) return true; // bare `key: {` fragment
+ if (/^(await |const |let |var )?[\w.$]+\([\s\S]*\)[;,]?$/.test(t) && !/\n/.test(t)) return false;
+ return false;
+}
+// JSON samples in docs conventionally carry // comments. Strip before parsing,
+// but still flag genuinely malformed JSON.
+const stripJsonComments = (s) => s.replace(/^\s*\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1');
+
+let failures = 0, checked = 0, skipped = 0;
+
+for (const file of files) {
+ const lines = fs.readFileSync(file, 'utf8').split('\n');
+ let open = false, lang = '', buf = [], start = 0, fences = 0;
+
+ for (let i = 0; i < lines.length; i++) {
+ const m = /^\s*```(.*)$/.exec(lines[i]);
+ if (m) {
+ fences++;
+ if (!open) {
+ open = true; start = i + 1; buf = [];
+ lang = (m[1].trim().split(/\s+/)[0] || '').toLowerCase();
+ } else {
+ open = false;
+ const norm = NORMALISE[lang] || lang;
+ const code = dedent(buf.join('\n'));
+ const checker = CHECKERS[norm];
+ if (checker && code.trim()) {
+ if (isFragment(code, norm)) { skipped++; continue; }
+ const payload = norm === 'json' ? stripJsonComments(code) : code;
+ const res = checker(payload);
+ if (res === null) { skipped++; continue; } // no runtime available
+ checked++;
+ if (!res.ok) {
+ failures++;
+ console.error(`\n✖ ${file}:${start} [${norm}]`);
+ console.error(` ${res.err}`);
+ console.error(` ${code.trim().split('\n')[0].slice(0, 90)}`);
+ }
+ }
+ }
+ continue;
+ }
+ if (open) buf.push(lines[i]);
+ }
+
+ if (fences % 2 !== 0) {
+ failures++;
+ console.error(`\n✖ ${file} unbalanced code fences (${fences}) — a block is never closed`);
+ console.error(' This silently inverts every following block: prose renders as code and code as prose.');
+ }
+ // Typographic quotes inside code blocks are a hard syntax error in every language.
+ const smart = [];
+ let inb = false;
+ lines.forEach((ln, i) => {
+ if (/^\s*```/.test(ln)) { inb = !inb; return; }
+ if (inb && /[‘’“”]/.test(ln)) smart.push(i + 1);
+ });
+ if (smart.length) {
+ failures++;
+ console.error(`\n✖ ${file} curly quotes inside code blocks at line(s) ${smart.slice(0, 8).join(', ')}${smart.length > 8 ? '…' : ''}`);
+ console.error(' Replace ‘ ’ “ ” with ASCII \' and ".');
+ }
+}
+
+const unavailable = Object.entries(HAVE).filter(([, v]) => !v).map(([k]) => k);
+console.log(
+ `\nlint-code-blocks: ${checked} blocks checked, ${skipped} skipped (fragment or no runtime), ` +
+ `${failures} problem(s) in ${files.length} file(s)` +
+ (unavailable.length ? `\n note: no runtime for ${unavailable.join(', ')} — those blocks were skipped` : '')
+);
+process.exit(failures ? 1 : 0);