| | import { describe, expect, test } from 'vitest' |
| |
|
| | import { runRule } from '../../lib/init-test' |
| | import { tableColumnIntegrity } from '../../lib/linting-rules/table-column-integrity' |
| |
|
| | describe(tableColumnIntegrity.names.join(' - '), () => { |
| | test('Valid table with consistent columns passes', async () => { |
| | const markdown = [ |
| | '| Artist | Album | Year |', |
| | '|--------|-------|------|', |
| | '| Beyoncé | Lemonade | 2016 |', |
| | '| Kendrick Lamar | DAMN. | 2017 |', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Table with extra columns causes error', async () => { |
| | const markdown = ['| Name | Age |', '|------|-----|', '| Alice | 25 | Extra |'].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(1) |
| | expect(errors[0].lineNumber).toBe(3) |
| | if ((errors[0] as any).detail) { |
| | expect((errors[0] as any).detail).toContain('Table row has 3 columns but header has 2') |
| | } else if (errors[0].errorDetail) { |
| | expect(errors[0].errorDetail).toContain('Table row has 3 columns but header has 2') |
| | } else { |
| | console.log('Error structure:', JSON.stringify(errors[0], null, 2)) |
| | expect(errors[0]).toHaveProperty('detail') |
| | } |
| | }) |
| |
|
| | test('Table with missing columns causes error', async () => { |
| | const markdown = ['| Name | Age | City |', '|------|-----|------|', '| Bob | 30 |'].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(1) |
| | expect(errors[0].lineNumber).toBe(3) |
| | if ((errors[0] as any).detail) { |
| | expect((errors[0] as any).detail).toContain('Table row has 2 columns but header has 3') |
| | } else if (errors[0].errorDetail) { |
| | expect(errors[0].errorDetail).toContain('Table row has 2 columns but header has 3') |
| | } else { |
| | console.log('Error structure:', JSON.stringify(errors[0], null, 2)) |
| | expect(errors[0]).toHaveProperty('detail') |
| | } |
| | }) |
| |
|
| | test('Liquid-only rows are ignored', async () => { |
| | const markdown = [ |
| | '| Feature | Status |', |
| | '|---------|--------|', |
| | '| {% ifversion ghes %} |', |
| | '| Advanced | Enabled |', |
| | '| {% endif %} |', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Escaped pipes (\\|) are not counted as column separators', async () => { |
| | const markdown = [ |
| | '| Command | Description |', |
| | '|---------|-------------|', |
| | '| `git log --oneline \\| head` | Shows recent commits |', |
| | '| `echo "hello \\| world"` | Prints text with pipe |', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Escaped pipes mixed with real separators work correctly', async () => { |
| | const markdown = [ |
| | '| Code | Output | Notes |', |
| | '|------|--------|-------|', |
| | '| `echo "a \\| b" \\| wc` | 1 | Pipe in string and command |', |
| | '| `grep "x" file` | matches | No pipes here |', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Liquid for/endfor statements are ignored in table rows', async () => { |
| | const markdown = [ |
| | '| Item | Details |', |
| | '|------|---------|', |
| | '| {% for item in collection %} |', |
| | '| Product A | Available |', |
| | '| Product B | Sold out |', |
| | '| {% endfor %} |', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Mixed liquid statements (ifversion, for, endif) are ignored', async () => { |
| | const markdown = [ |
| | '| Feature | Status | Version |', |
| | '|---------|--------|---------|', |
| | '| {% ifversion ghes %} |', |
| | '| {% for version in site.data.versions %} |', |
| | '| Basic | Active | 1.0 |', |
| | '| {% endfor %} |', |
| | '| {% endif %} |', |
| | '| Advanced | Beta | 2.0 |', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Tables inside code fences are ignored', async () => { |
| | const markdown = [ |
| | 'Here is some example markdown:', |
| | '', |
| | '```markdown', |
| | '| Name | Age |', |
| | '|------|-----|', |
| | '| Alice | 25 | Extra column that would normally cause error |', |
| | '| Bob |', |
| | '```', |
| | '', |
| | 'But this real table should be validated:', |
| | '', |
| | '| Product | Price |', |
| | '|---------|-------|', |
| | '| Widget | $10 |', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Tables inside different code fence types are ignored', async () => { |
| | const markdown = [ |
| | '```', |
| | '| Malformed | Table |', |
| | '|-----------|-------|', |
| | '| Too | Many | Columns | Here |', |
| | '```', |
| | '', |
| | '```text', |
| | '| Another | Bad |', |
| | '|---------|-----|', |
| | '| Missing |', |
| | '```', |
| | '', |
| | '```yaml', |
| | '| YAML | Example |', |
| | '|------|---------|', |
| | '| key: | value | extra |', |
| | '```', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('File paths with pipes are handled correctly (regression test)', async () => { |
| | |
| | |
| | const markdown = [ |
| | '| Directory | Ubuntu | macOS |', |
| | '|-----------|--------|-------|', |
| | '|**Tool Cache Directory** |`/opt/hostedtoolcache/*`|`/Users/runner/hostedtoolcache/*`|', |
| | '|**Python Tool Cache**|`/opt/hostedtoolcache/Python/*`|`/Users/runner/hostedtoolcache/Python/*`|', |
| | '|**PyPy Tool Cache**|`/opt/hostedtoolcache/PyPy/*`|`/Users/runner/hostedtoolcache/PyPy/*`|', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Complex file paths with multiple characters before pipes', async () => { |
| | |
| | const markdown = [ |
| | '| Pattern | Linux Path | Windows Path |', |
| | '|---------|------------|--------------|', |
| | '| Cache | `/home/user/.cache/*` | `C:\\Users\\user\\AppData\\*` |', |
| | '| Logs | `/var/log/app/*` | `C:\\ProgramData\\logs\\*` |', |
| | '| Config | `/etc/myapp/*` | `C:\\Program Files\\MyApp\\*` |', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Code fence spanning multiple lines with tables inside', async () => { |
| | const markdown = [ |
| | 'Here is some documentation:', |
| | '', |
| | '```markdown', |
| | '# Example Document', |
| | '', |
| | '| Bad | Table |', |
| | '|-----|-------|', |
| | '| Missing | column | here | extra |', |
| | '| Another | bad | row |', |
| | '', |
| | 'More content here', |
| | '```', |
| | '', |
| | 'This real table should be validated:', |
| | '', |
| | '| Good | Table |', |
| | '|------|-------|', |
| | '| Valid | Row |', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Multiple code fences with tables between them', async () => { |
| | const markdown = [ |
| | '```js', |
| | '| Bad | JS | Table |', |
| | '|-----|----|----|', |
| | '| Extra | column | here | bad |', |
| | '```', |
| | '', |
| | 'Real table that should be checked:', |
| | '', |
| | '| Name | Status |', |
| | '|------|--------|', |
| | '| Test | Pass |', |
| | '', |
| | '```bash', |
| | '| Command | Output |', |
| | '|---------|--------|', |
| | '| ls | file1.txt | file2.txt | extra |', |
| | '```', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| |
|
| | test('Code fence with language identifier', async () => { |
| | const markdown = [ |
| | '```typescript', |
| | 'const badTable = `', |
| | '| Name | Age |', |
| | '|------|-----|', |
| | '| Alice | 25 | Extra |', |
| | '`', |
| | '```', |
| | '', |
| | '```yaml', |
| | 'table:', |
| | ' - name: Bad', |
| | ' - age: 30', |
| | ' - extra: column', |
| | '```', |
| | ].join('\n') |
| | const result = await runRule(tableColumnIntegrity, { strings: { markdown } }) |
| | const errors = result.markdown |
| | expect(errors.length).toBe(0) |
| | }) |
| | }) |
| |
|