From 5d2d0f139129af1c4e56f272f883ced51edc0a1a Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Tue, 3 Dec 2024 19:39:24 +0800 Subject: [PATCH 01/12] Add test file StyleParser.test.ts for ink-css --- package.json | 5 +- packages/ink-css/package.json | 11 +- packages/ink-css/tests/StyleParser.test.ts | 161 +++++++++++++++++++++ packages/ink/package.json | 21 +-- 4 files changed, 184 insertions(+), 14 deletions(-) create mode 100644 packages/ink-css/tests/StyleParser.test.ts diff --git a/package.json b/package.json index 69bd99f..c377de8 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,9 @@ "build:tailwind": "yarn --cwd packages/ink-tailwind build", "build:ui": "yarn --cwd packages/ink-ui-src build", "build:web": "yarn --cwd packages/ink-web build", - "test": "yarn --cwd packages/ink test", - "test:web": "yarn --cwd packages/ink-web test", + "test": "yarn test:ink && yarn test:css", + "test:ink":"yarn --cwd packages/ink test", + "test:css": "yarn --cwd packages/ink-css test", "dev:http": "yarn --cwd examples/with-http dev", "dev:express": "yarn --cwd examples/with-express dev", "dev:fastify": "yarn --cwd examples/with-fastify dev", diff --git a/packages/ink-css/package.json b/packages/ink-css/package.json index 3837a34..cc13af6 100644 --- a/packages/ink-css/package.json +++ b/packages/ink-css/package.json @@ -17,13 +17,20 @@ "tsconfig.json" ], "scripts": { - "build": "tsc" + "build": "tsc", + "test": "nyc ts-mocha tests/*.test.ts" }, "dependencies": { - "@stackpress/ink": "0.3.6" + "@stackpress/ink": "0.2.10" }, "devDependencies": { + "@types/chai":"5.0.1", + "@types/mocha": "10.0.10", "@types/node": "22.9.3", + "chai":"5.1.2", + "mocha":"11.0.0", + "nyc":"17.1.0", + "ts-mocha":"10.0.0", "ts-node": "10.9.2", "typescript": "5.7.2" } diff --git a/packages/ink-css/tests/StyleParser.test.ts b/packages/ink-css/tests/StyleParser.test.ts new file mode 100644 index 0000000..12dd65e --- /dev/null +++ b/packages/ink-css/tests/StyleParser.test.ts @@ -0,0 +1,161 @@ +import { expect } from 'chai'; +import { default as StyleParser, pattern } from '../src/StyleParser'; +import NodeFS from '@stackpress/types/dist/system/NodeFS'; +import path from 'path'; +import fs from 'fs'; + +describe('StyleParser', () => { + describe('pattern', () => { + it('should match valid class names', () => { + const validClasses = [ + 'abc', + 'abc-def', + 'abc-1def', + 'abc-1def-2ghi' + ]; + + validClasses.forEach(className => { + const matches = className.match(pattern); + expect(matches).to.not.be.null; + expect(matches![0]).to.equal(className); + }); + }); + + it('should not match invalid class names', () => { + const invalidClasses = [ + 'ABC', // uppercase not allowed + '@abc', // special characters not allowed + 'abc_def' // underscore not allowed + ]; + + invalidClasses.forEach(className => { + // Test each invalid class name individually against the pattern + const regex = new RegExp('^' + pattern.source + '$'); + const matches = className.match(regex); + expect(matches, `${className} should not match`).to.be.null; + }); + }); + }); + + describe('StyleParser class', () => { + let parser: StyleParser; + let tempDir: string; + let testFile: string; + + beforeEach(() => { + parser = new StyleParser({ + cwd: process.cwd() + }); + + // Create a temporary test directory and file + tempDir = path.join(process.cwd(), 'temp_test_dir'); + testFile = path.join(tempDir, 'test.css'); + fs.mkdirSync(tempDir, { recursive: true }); + }); + + afterEach(() => { + // Clean up temporary files + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + describe('static match()', () => { + it('should return unique class names from content', () => { + const content = 'some-class other-class some-class third-class'; + const matches = StyleParser.match(content); + expect(matches).to.deep.equal(['some-class', 'other-class', 'third-class']); + }); + + it('should return empty array for content with no matches', () => { + const content = 'NO_VALID_CLASSES_HERE'; + const matches = StyleParser.match(content); + expect(matches).to.deep.equal([]); + }); + }); + + describe('instance methods', () => { + it('should initialize with default options', () => { + const defaultParser = new StyleParser(); + expect(defaultParser.cwd).to.equal(process.cwd()); + expect(defaultParser.fs).to.be.instanceOf(NodeFS); + }); + + it('should initialize with custom options', () => { + const customFs = new NodeFS(); + const customCwd = process.cwd() + '/custom'; + const customParser = new StyleParser({ fs: customFs, cwd: customCwd }); + + expect(customParser.cwd).to.equal(customCwd); + expect(customParser.fs).to.equal(customFs); + }); + + it('should throw exception for non-existent file', () => { + const nonExistentFile = 'non-existent-file.txt'; + const absolutePath = parser.loader.absolute(nonExistentFile); + // First add the file to the cache using the absolute path + parser.vfs.set(absolutePath, 'some content'); + // Now try to add the non-existent file, which should throw + expect(() => parser.add(nonExistentFile)).to.throw('File not found'); + }); + + it('should successfully read and cache file content', () => { + // Create a test file + const fileContent = 'test-class another-class'; + fs.writeFileSync(testFile, fileContent); + + // Add the file to the parser + const absolutePath = parser.loader.absolute(testFile); + parser.vfs.clear(); // Clear the cache first + + // First set it in the cache (required by implementation) + parser.vfs.set(absolutePath, ''); + parser.add(testFile); + + // Verify the file was cached with updated content + expect(parser.vfs.has(absolutePath)).to.be.true; + expect(parser.vfs.get(absolutePath)).to.equal(fileContent); + }); + + it('should skip adding file if not in cache', () => { + // Create a test file + const fileContent = 'test-class'; + fs.writeFileSync(testFile, fileContent); + + // Add the file without setting it in cache first + const absolutePath = parser.loader.absolute(testFile); + parser.vfs.clear(); // Clear the cache first + parser.add(testFile); + + // Verify the file was not added to cache + expect(parser.vfs.has(absolutePath)).to.be.false; + }); + + it('should parse all cached files and return unique class names', () => { + // Add multiple files to the cache + parser.vfs.clear(); // Clear the cache first + parser.set('file1.css', 'class1 shared-class'); + parser.set('file2.css', 'class2 shared-class'); + + const classNames = parser.parse(); + expect(classNames).to.deep.equal(['class1', 'shared-class', 'class2', 'shared-class']); + }); + + it('should walk through cached files and yield class names', () => { + // Add multiple files to the cache + parser.vfs.clear(); // Clear the cache first + parser.set('file1.css', 'class1 shared-class'); + parser.set('file2.css', 'class2 shared-class'); + + const classNames = Array.from(parser.walk()); + expect(classNames).to.deep.equal(['class1', 'shared-class', 'class2', 'shared-class']); + }); + + it('should handle empty cache in parse and walk', () => { + parser.vfs.clear(); // Clear the cache first + expect(parser.parse()).to.deep.equal([]); + expect(Array.from(parser.walk())).to.deep.equal([]); + }); + }); + }); +}); diff --git a/packages/ink/package.json b/packages/ink/package.json index 3b4da78..1a0a4b2 100644 --- a/packages/ink/package.json +++ b/packages/ink/package.json @@ -28,24 +28,25 @@ "README.md", "tsconfig.json" ], - "scripts": { + "scripts": { "build": "tsc", - "test": "ts-mocha tests/*.test.ts" + "test": "nyc ts-mocha tests/*.test.ts" }, "dependencies": { - "@stackpress/types": "0.3.6", + "@stackpress/types": "0.2.10", "esbuild": "0.24.0", "ts-morph": "24.0.0", "typescript": "5.7.2" }, "devDependencies": { - "@types/chai": "4.3.20", + "@types/chai":"5.0.1", "@types/mocha": "10.0.10", "@types/node": "22.9.3", - "chai": "4.5.0", - "mocha": "10.8.2", - "nyc": "17.1.0", - "ts-mocha": "10.0.0", - "ts-node": "10.9.2" + "chai":"5.1.2", + "mocha":"11.0.0", + "nyc":"17.1.0", + "ts-mocha":"10.0.0", + "ts-node": "10.9.2", + "typescript": "5.7.2" } -} \ No newline at end of file +} From 5e6a6664c1752b4886a2322b6d3523b3862720bd Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Tue, 3 Dec 2024 19:47:53 +0800 Subject: [PATCH 02/12] Add test file EventEmitter.test.ts for ink --- packages/ink/tests/EventEmitter.test.ts | 112 ++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/ink/tests/EventEmitter.test.ts diff --git a/packages/ink/tests/EventEmitter.test.ts b/packages/ink/tests/EventEmitter.test.ts new file mode 100644 index 0000000..0be0b4e --- /dev/null +++ b/packages/ink/tests/EventEmitter.test.ts @@ -0,0 +1,112 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import InkEventEmitter, { Event } from '../src/EventEmitter'; + +describe('Event', () => { + it('should create an event with name and params', () => { + const event = new Event('test', { foo: 'bar' }); + expect(event.name).to.equal('test'); + expect(event.params).to.deep.equal({ foo: 'bar' }); + }); + + it('should allow adding params after creation', () => { + const event = new Event('test'); + event.add('key', 'value'); + expect(event.params.key).to.equal('value'); + }); + + it('should handle setting and getting data', () => { + const event = new Event('test'); + event.set('test data'); + expect(event.data).to.equal('test data'); + }); + + it('should handle data property setter', () => { + const event = new Event('test'); + event.data = 'test data'; + expect(event.data).to.equal('test data'); + }); +}); + +describe('InkEventEmitter', () => { + it('should trigger events and pass event object', () => { + const emitter = new InkEventEmitter(); + let receivedEvent: Event | null = null; + + emitter.on('test', (event) => { + receivedEvent = event; + }); + const event = emitter.trigger('test', { foo: 'bar' }); + + expect(receivedEvent).to.not.be.null; + expect(event.name).to.equal('test'); + expect(event.params).to.deep.equal({ foo: 'bar' }); + }); + + it('should handle multiple listeners', () => { + const emitter = new InkEventEmitter(); + let count = 0; + + emitter.on('test', () => count++); + emitter.on('test', () => count++); + emitter.trigger('test'); + + expect(count).to.equal(2); + }); + + it('should handle event data in listeners', () => { + const emitter = new InkEventEmitter(); + + emitter.on('test', (event: Event) => { + event.set('response data'); + }); + + const event = emitter.trigger('test'); + expect(event.data).to.equal('response data'); + }); + + it('should handle async event processing with waitFor', async () => { + const emitter = new InkEventEmitter(); + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + emitter.on('test', async (event: Event) => { + await delay(10); + event.set('async response'); + }); + + const event = await emitter.waitFor('test'); + expect(event.data).to.equal('async response'); + }); + + it('should process multiple async listeners in sequence', async () => { + const emitter = new InkEventEmitter(); + const results: number[] = []; + + emitter.on('test', async () => { + await new Promise(resolve => setTimeout(resolve, 20)); + results.push(1); + }); + + emitter.on('test', async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + results.push(2); + }); + + await emitter.waitFor('test'); + expect(results).to.deep.equal([1, 2]); + }); + + it('should remove event listeners correctly', () => { + const emitter = new InkEventEmitter(); + let count = 0; + const listener = () => count++; + + emitter.on('test', listener); + emitter.trigger('test'); + expect(count).to.equal(1); + + emitter.off('test', listener); + emitter.trigger('test'); + expect(count).to.equal(1); + }); +}); \ No newline at end of file From bd6dcbc8bc43a7e83bf6a93475c75f6610987fd4 Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Tue, 3 Dec 2024 21:07:01 +0800 Subject: [PATCH 03/12] Add test file helpers.test.ts for ink --- packages/ink/tests/fixtures/test.js | 5 + packages/ink/tests/helpers.test.ts | 238 ++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 packages/ink/tests/fixtures/test.js create mode 100644 packages/ink/tests/helpers.test.ts diff --git a/packages/ink/tests/fixtures/test.js b/packages/ink/tests/fixtures/test.js new file mode 100644 index 0000000..f09fca5 --- /dev/null +++ b/packages/ink/tests/fixtures/test.js @@ -0,0 +1,5 @@ +export function greet(name) { + return `Hello, ${name}!`; +} + +export const VERSION = '1.0.0'; diff --git a/packages/ink/tests/helpers.test.ts b/packages/ink/tests/helpers.test.ts new file mode 100644 index 0000000..792a0d4 --- /dev/null +++ b/packages/ink/tests/helpers.test.ts @@ -0,0 +1,238 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import type { SourceFile, OutputFile, EmitOutput } from 'ts-morph'; +import * as vm from 'vm'; +import path from 'path'; +import { + camelize, + capitalize, + lowerlize, + serialize, + slugify, + toJS, + toTS, + load, + build +} from '../src/helpers'; + +describe('helpers', () => { + describe('camelize', () => { + it('should convert string to camelCase', () => { + expect(camelize('some string')).to.equal('SomeString'); + expect(camelize('some-string')).to.equal('SomeString'); + expect(camelize('some_string')).to.equal('SomeString'); + expect(camelize('some__string')).to.equal('SomeString'); + expect(camelize(' some string ')).to.equal('SomeString'); + }); + + it('should convert string to camelCase with lower first letter', () => { + expect(camelize('some string', true)).to.equal('someString'); + expect(camelize('some-string', true)).to.equal('someString'); + expect(camelize('some_string', true)).to.equal('someString'); + }); + }); + + describe('capitalize', () => { + it('should capitalize first letter', () => { + expect(capitalize('word')).to.equal('Word'); + expect(capitalize('Word')).to.equal('Word'); + expect(capitalize('')).to.equal(''); + }); + }); + + describe('lowerlize', () => { + it('should lowercase first letter', () => { + expect(lowerlize('Word')).to.equal('word'); + expect(lowerlize('word')).to.equal('word'); + expect(lowerlize('')).to.equal(''); + }); + }); + + describe('serialize', () => { + it('should create consistent hash for same input', () => { + const input = 'test string'; + const hash1 = serialize(input); + const hash2 = serialize(input); + expect(hash1).to.equal(hash2); + expect(hash1).to.have.lengthOf(20); // shake256 with outputLength 10 produces 20 hex chars + }); + + it('should create different hashes for different inputs', () => { + const hash1 = serialize('test1'); + const hash2 = serialize('test2'); + expect(hash1).to.not.equal(hash2); + }); + }); + + describe('slugify', () => { + it('should convert string to slug format', () => { + expect(slugify('Some Title')).to.equal('some-title'); + expect(slugify('SomeTitle')).to.equal('sometitle'); + expect(slugify(' Some Title ')).to.equal('some-title'); + expect(slugify('Some-Title')).to.equal('some-title'); + expect(slugify('Some__Title')).to.equal('some-title'); + }); + }); + + describe('toJS', () => { + it('should convert source file to javascript', () => { + // Mock SourceFile and its methods + const mockOutputFile: Partial = { + getFilePath: () => 'test.js' as any, + getText: () => 'const test = "hello";' + }; + + const mockEmitOutput: Partial = { + getOutputFiles: () => [mockOutputFile as OutputFile], + compilerObject: { + outputFiles: [], + emitSkipped: false, + diagnostics: [] + } as any, + getDiagnostics: () => [], + getEmitSkipped: () => false + }; + + const mockSourceFile: Partial = { + getEmitOutput: () => mockEmitOutput as EmitOutput + }; + + const result = toJS(mockSourceFile as SourceFile); + expect(result).to.equal('const test = "hello";'); + }); + + it('should handle multiple output files', () => { + const mockOutputFiles: Partial[] = [ + { + getFilePath: () => 'test.d.ts' as any, + getText: () => 'declare const test: string;' + }, + { + getFilePath: () => 'test.js' as any, + getText: () => 'const test = "hello";' + } + ]; + + const mockEmitOutput: Partial = { + getOutputFiles: () => mockOutputFiles as OutputFile[], + compilerObject: { + outputFiles: [], + emitSkipped: false, + diagnostics: [] + } as any, + getDiagnostics: () => [], + getEmitSkipped: () => false + }; + + const mockSourceFile: Partial = { + getEmitOutput: () => mockEmitOutput as EmitOutput + }; + + const result = toJS(mockSourceFile as SourceFile); + expect(result).to.equal('const test = "hello";'); + }); + }); + + describe('toTS', () => { + it('should get full text from source file', () => { + const mockSourceFile: Partial = { + getFullText: () => 'const test: string = "hello";' + }; + + const result = toTS(mockSourceFile as SourceFile); + expect(result).to.equal('const test: string = "hello";'); + }); + }); + + describe('load', () => { + it('should load and execute javascript in vm context', () => { + const source = ` + exports.testValue = "hello"; + exports.testFunc = function() { return "world"; }; + `; + + const context = load(source); + expect(context.exports).to.have.property('testValue', 'hello'); + expect(context.exports).to.have.property('testFunc'); + expect(context.exports.testFunc()).to.equal('world'); + }); + + it('should provide necessary global objects', () => { + const source = ` + exports.hasConsole = typeof console !== 'undefined'; + exports.hasModule = typeof module !== 'undefined'; + exports.hasRequire = typeof require !== 'undefined'; + exports.hasProcess = typeof process !== 'undefined'; + exports.hasBtoa = typeof btoa !== 'undefined'; + exports.hasAtob = typeof atob !== 'undefined'; + `; + + const context = load(source); + expect(context.exports.hasConsole).to.be.true; + expect(context.exports.hasModule).to.be.true; + expect(context.exports.hasRequire).to.be.true; + expect(context.exports.hasProcess).to.be.true; + expect(context.exports.hasBtoa).to.be.true; + expect(context.exports.hasAtob).to.be.true; + }); + + it('should handle errors in script execution', () => { + const source = 'throw new Error("test error");'; + expect(() => load(source)).to.throw('test error'); + }); + }); + + describe('build', () => { + const testFilePath = path.join(__dirname, 'fixtures', 'test.js'); + + it('should build with default options', async () => { + const result = await build(testFilePath); + expect(result).to.be.a('string'); + }); + + it('should build with custom format', async () => { + const result = await build(testFilePath, { + format: 'esm', + minify: false + }); + expect(result).to.be.a('string'); + expect(result).to.include('export'); // ESM format should have export statements + }); + + it('should build with global name', async () => { + const result = await build(testFilePath, { + format: 'iife', + globalName: 'TestModule' + }); + expect(result).to.be.a('string'); + expect(result).to.include('TestModule'); + }); + + it('should build without bundling', async () => { + const result = await build(testFilePath, { + bundle: false, + minify: false + }); + expect(result).to.be.a('string'); + }); + + it('should build with custom platform', async () => { + const result = await build(testFilePath, { + platform: 'node' + }); + expect(result).to.be.a('string'); + }); + + it('should build with custom plugins', async () => { + const mockPlugin = { + name: 'test-plugin', + setup: () => {} + }; + + const result = await build(testFilePath, { + plugins: [mockPlugin] + }); + expect(result).to.be.a('string'); + }); + }); +}); From bc9d55cd3746e16e3bcad6728093a4681ceea8b4 Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Wed, 4 Dec 2024 07:35:41 +0800 Subject: [PATCH 04/12] Add test file plugins.test.ts for ink --- packages/ink/tests/plugins.test.ts | 152 +++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 packages/ink/tests/plugins.test.ts diff --git a/packages/ink/tests/plugins.test.ts b/packages/ink/tests/plugins.test.ts new file mode 100644 index 0000000..c5c4267 --- /dev/null +++ b/packages/ink/tests/plugins.test.ts @@ -0,0 +1,152 @@ +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import path from 'node:path'; +import type { Stats } from 'fs'; +import type { PluginBuild } from 'esbuild'; +import NodeFS from '@stackpress/types/dist/system/NodeFS'; +import { + esAliasPlugin, + esComponentPlugin, + esDocumentPlugin, + esWorkspacePlugin, + esInkPlugin +} from '../src/plugins'; + +class MockStats implements Stats { + isFile(): boolean { return true; } + isDirectory(): boolean { return false; } + isBlockDevice(): boolean { return false; } + isCharacterDevice(): boolean { return false; } + isSymbolicLink(): boolean { return false; } + isFIFO(): boolean { return false; } + isSocket(): boolean { return false; } + dev: number = 0; + ino: number = 0; + mode: number = 0; + nlink: number = 1; + uid: number = 0; + gid: number = 0; + rdev: number = 0; + size: number = 0; + blksize: number = 0; + blocks: number = 0; + atimeMs: number = 0; + mtimeMs: number = 0; + ctimeMs: number = 0; + birthtimeMs: number = 0; + atime: Date = new Date(); + mtime: Date = new Date(); + ctime: Date = new Date(); + birthtime: Date = new Date(); +} + +class MockFS extends NodeFS { + private files: Map = new Map(); + + constructor(cwd: string = '/test') { + super(); + // Add some mock files with absolute paths + this.files.set(path.resolve(cwd, 'component.ink'), { content: '', isFile: true }); + this.files.set(path.resolve(cwd, 'script.ts'), { content: '', isFile: true }); + this.files.set(path.resolve(cwd, 'index.ts'), { content: '', isFile: true }); + } + + existsSync(filepath: string): boolean { + return this.files.has(path.resolve(filepath)); + } + + lstatSync(filepath: string): Stats { + const file = this.files.get(path.resolve(filepath)); + const stats = new MockStats(); + Object.defineProperty(stats, 'isFile', { + value: () => file?.isFile ?? false + }); + return stats; + } +} + +describe('plugins', () => { + describe('esAliasPlugin', () => { + it('should resolve @/ paths correctly', () => { + const cwd = process.platform === 'win32' ? 'c:\\test' : '/test'; + const fs = new MockFS(cwd); + const plugin = esAliasPlugin({ cwd, fs }); + + let resolveResult: any; + const build: Partial = { + onResolve: ({ filter }: any, callback: any) => { + expect(filter.toString()).to.equal('/^@\\//'); + resolveResult = callback({ + path: '@/component.ink', + resolveDir: cwd + }); + } + }; + + plugin.setup(build as PluginBuild); + const expectedPath = process.platform === 'win32' + ? 'c:\\test\\component.ink' + : '/test/component.ink'; + expect(resolveResult).to.deep.equal({ + path: expectedPath, + namespace: 'ink-component-plugin' + }); + }); + + it('should handle different file extensions', () => { + const cwd = process.platform === 'win32' ? 'c:\\test' : '/test'; + const fs = new MockFS(cwd); + const plugin = esAliasPlugin({ cwd, fs }); + + let resolveResult: any; + const build: Partial = { + onResolve: ({ filter }: any, callback: any) => { + expect(filter.toString()).to.equal('/^@\\//'); + resolveResult = callback({ + path: '@/script.ts', + resolveDir: cwd + }); + } + }; + + plugin.setup(build as PluginBuild); + const expectedPath = process.platform === 'win32' + ? 'c:\\test\\script.ts' + : '/test/script.ts'; + expect(resolveResult).to.deep.equal({ + path: expectedPath, + loader: 'ts' + }); + }); + }); + + describe('esComponentPlugin', () => { + it('should initialize with default options', () => { + const plugin = esComponentPlugin(); + expect(plugin.name).to.equal('ink-component-plugin'); + }); + }); + + describe('esDocumentPlugin', () => { + it('should initialize with default options', () => { + const plugin = esDocumentPlugin(); + expect(plugin.server.name).to.equal('ink-document-server-plugin'); + expect(plugin.client.name).to.equal('ink-document-client-plugin'); + }); + }); + + describe('esWorkspacePlugin', () => { + it('should initialize correctly', () => { + const plugin = esWorkspacePlugin(); + expect(plugin.name).to.equal('resolve-workspace-packages'); + }); + }); + + describe('esInkPlugin', () => { + it('should combine all plugins', () => { + const plugin = esInkPlugin(); + expect(plugin).to.have.property('name', 'ink-plugin'); + expect(plugin).to.have.property('setup').that.is.a('function'); + }); + }); +}); From 479a8625d327fbad034232ed616dba8971e64c38 Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Wed, 4 Dec 2024 20:28:00 +0800 Subject: [PATCH 05/12] Add test file ConditionalDirective.test.ts for ink --- .../ink/tests/ConditionalDirective.test.ts | 528 ++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 packages/ink/tests/ConditionalDirective.test.ts diff --git a/packages/ink/tests/ConditionalDirective.test.ts b/packages/ink/tests/ConditionalDirective.test.ts new file mode 100644 index 0000000..43fd48e --- /dev/null +++ b/packages/ink/tests/ConditionalDirective.test.ts @@ -0,0 +1,528 @@ +import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; +import { IfDirective, ElifDirective, ElseDirective } +from '../src/directives/ConditionalDirective'; +import type { MarkupToken, PropertyToken, ScriptToken, + ObjectToken, NextDirective, MarkupChildToken } from '../src/types'; +import Component from '../src/compiler/Component'; +import type Transpiler from '../src/compiler/Transpiler'; + +describe('ConditionalDirective', () => { + // Create a mock transpiler + const mockTranspiler = { + // Add any required transpiler methods here + } as Transpiler; + + // Helper function to create a mock next directive function + const mockNext: NextDirective = (parent: MarkupToken|null, + token: MarkupChildToken[], components: Component[]) => { + return '[child content]'; + }; + + describe('IfDirective', () => { + let ifDirective: IfDirective; + + beforeEach(() => { + ifDirective = new IfDirective(mockTranspiler); + }); + + // Test directive name + it('should have correct directive name', () => { + expect(ifDirective.name).to.equal('if'); + }); + + // Test invalid if statement (no attributes) + it('should throw error for if statement without attributes', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: undefined, + children: [] + }; + expect(() => { + ifDirective.markup(null, token, [], mockNext); + }).to.throw('Invalid if statement'); + }); + + // Test if statement with boolean literal + it('should handle boolean literal true condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: true, raw: 'true', + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!!(true) ? [child content] : [])'); + }); + + // Test if statement with string literal + it('should handle string literal condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: 'hello', raw: "'hello'", + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal("...(!!('hello') ? [child content] : [])"); + }); + + // Test if statement with program expression + it('should handle program expression condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { + type: 'ProgramExpression', + start: 0, + end: 10, + inline: true, + source: 'count > 1', + runtime: false + } as ScriptToken, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!!(count > 1) ? [child content] : [])'); + }); + + // Test if statement with children + it('should handle if statement with children', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Identifier', name: 'isVisible', start: 0, end: 4 }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [{ + type: 'MarkupExpression', + name: 'div', + kind: 'block', + start: 0, + end: 10, + children: [] + }] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!!(isVisible) ? [child content] : [])'); + }); + + // Test if statement with identifier + it('should handle identifier condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Identifier', name: 'isVisible', start: 0, end: 4 }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!!(isVisible) ? [child content] : [])'); + }); + + // Test if statement with false condition + it('should handle false condition', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'false', start: 0, end: 4 }, + value: { type: 'Identifier', name: 'isHidden', start: 0, end: 4 }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = ifDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(!(isHidden) ? [child content] : [])'); + }); + + // Test invalid property type + it('should throw error for invalid property type', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'ArrayExpression', elements: [], start: 0, end: 4 }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + expect(() => { + ifDirective.markup(null, token, [], mockNext); + }).to.throw('Invalid if statement'); + }); + }); + + describe('ElifDirective', () => { + let elifDirective: ElifDirective; + + beforeEach(() => { + elifDirective = new ElifDirective(mockTranspiler); + }); + + it('should have correct directive name', () => { + expect(elifDirective.name).to.equal('elif'); + }); + + // Test elif without parent + it('should throw error when no parent', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'elif', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: true, raw: 'true', + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + expect(() => { + elifDirective.markup(null, token); + }).to.throw('Invalid elif statement'); + }); + + // Test elif with wrong parent type + it('should throw error when parent is not if', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'div', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'elif', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: true, raw: 'true', + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + expect(() => { + elifDirective.markup(parent, token); + }).to.throw('Invalid elif statement'); + }); + + // Test valid elif with program expression + it('should handle program expression condition', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'elif', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { + type: 'ProgramExpression', + start: 0, + end: 10, + inline: true, + source: 'count === 2', + runtime: false + } as ScriptToken, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = elifDirective.markup(parent, token); + expect(result).to.equal(']: !!(count === 2) ? [ '); + }); + }); + + describe('ElseDirective', () => { + let elseDirective: ElseDirective; + + beforeEach(() => { + elseDirective = new ElseDirective(mockTranspiler); + }); + + it('should have correct directive name', () => { + expect(elseDirective.name).to.equal('else'); + }); + + // Test else without parent + it('should throw error when no parent', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'else', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + expect(() => { + elseDirective.markup(null, token); + }).to.throw('Invalid else statement'); + }); + + // Test else with wrong parent type + it('should throw error when parent is not if', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'div', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'else', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + expect(() => { + elseDirective.markup(parent, token); + }).to.throw('Invalid else statement'); + }); + + // Test valid else directive + it('should handle valid else statement', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'else', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const result = elseDirective.markup(parent, token); + expect(result).to.equal(']: true ? ['); + }); + + // Test else ignores attributes + it('should ignore attributes in else statement', () => { + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'if', + kind: 'block', + start: 0, + end: 10, + children: [] + }; + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'else', + kind: 'block', + start: 0, + end: 10, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 10, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 10, + key: { type: 'Identifier', name: 'true', start: 0, end: 4 }, + value: { type: 'Literal', value: true, raw: 'true', + start: 0, end: 4, escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }] as PropertyToken[] + } as ObjectToken, + children: [] + }; + const result = elseDirective.markup(parent, token); + expect(result).to.equal(']: true ? ['); + }); + }); +}); \ No newline at end of file From f649b1a44147f11634aada8f5ebe274baea10319 Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Wed, 4 Dec 2024 23:51:06 +0800 Subject: [PATCH 06/12] Add test file IteratorDirecive.test.ts for ink --- .../ink/src/directives/IteratorDirective.ts | 40 +++- packages/ink/tests/IteratorDirective.test.ts | 192 ++++++++++++++++++ 2 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 packages/ink/tests/IteratorDirective.test.ts diff --git a/packages/ink/src/directives/IteratorDirective.ts b/packages/ink/src/directives/IteratorDirective.ts index 159127a..1dc85b3 100644 --- a/packages/ink/src/directives/IteratorDirective.ts +++ b/packages/ink/src/directives/IteratorDirective.ts @@ -12,16 +12,30 @@ import Parser from '../compiler/Parser'; //local import AbstractDirective from './AbstractDirective'; +/** + * IteratorDirective implements the 'each' directive for iterating over arrays and objects + * in templates. It supports the following syntax: + * + * Arrays: ... + * Objects: ... + * Expressions: ... + */ export default class IteratorDirective extends AbstractDirective { /** - * Returns the directive name + * Returns the directive name used in templates */ public get name() { return 'each'; } /** - * Saves the compiler instance + * Processes the markup token and generates the iteration code + * @param parent - The parent markup token (if any) + * @param token - The current markup token being processed + * @param components - List of available components + * @param next - Function to process child elements + * @returns Generated JavaScript code for iteration + * @throws Exception if the iteration syntax is invalid */ public markup( parent: MarkupToken|null, @@ -30,12 +44,15 @@ export default class IteratorDirective extends AbstractDirective { next: NextDirective ) { let expression = ''; - //syntax ... + + // Validate that attributes are present if (!token.attributes || token.attributes.properties.length === 0 ) { throw Exception.for('Invalid each statement'); } + + // Extract key, value, and from attributes const key = token.attributes.properties.find( property => property.key.name === 'key' ); @@ -45,6 +62,8 @@ export default class IteratorDirective extends AbstractDirective { const from = token.attributes.properties.find( property => property.key.name === 'from' ); + + // Validate required attributes if (!from || (!key && !value)) { throw Exception.for('Invalid each statement'); } else if (key && key.value.type !== 'Identifier') { @@ -52,31 +71,46 @@ export default class IteratorDirective extends AbstractDirective { } else if (value && value.value.type !== 'Identifier') { throw Exception.for('Invalid value in each'); } + + // Use default '_' for unused key/value names const keyName = (key?.value as IdentifierToken)?.name || '_'; const valueName = (value?.value as IdentifierToken)?.name || '_'; + expression += `...`; + + // Handle different types of 'from' values if (from.value.type === 'ProgramExpression') { + // For JavaScript expressions (e.g., array.filter()) const script = from.value as ScriptToken; expression += `Object.entries(${script.source})`; } else if (from.value.type === 'ArrayExpression') { + // For literal arrays (e.g., ['a', 'b', 'c']) expression += `Object.entries(${ JSON.stringify(Parser.array(from.value)) })`; } else if (from.value.type === 'ObjectExpression') { + // For literal objects (e.g., {a: 1, b: 2}) expression += `Object.entries(${ JSON.stringify(Parser.object(from.value)) })`; } else if (from.value.type === 'Identifier') { + // For variables (e.g., items) expression += `Object.entries(${from.value.name})`; } else { throw Exception.for('Invalid from value in each'); } + + // Generate map function with destructuring for key/value pairs expression += `.map(([${keyName}, ${valueName}]) => `; + + // Process child elements if they exist if (token.children) { expression += next(token, token.children, components); } else { expression += '[]'; } + + // Flatten the results to handle nested arrays expression += ').flat()'; return expression; } diff --git a/packages/ink/tests/IteratorDirective.test.ts b/packages/ink/tests/IteratorDirective.test.ts new file mode 100644 index 0000000..51b1fd1 --- /dev/null +++ b/packages/ink/tests/IteratorDirective.test.ts @@ -0,0 +1,192 @@ +import { expect } from 'chai'; +import IteratorDirective from '../src/directives/IteratorDirective'; +import type { MarkupToken, NextDirective, MarkupChildToken } from '../src/types'; +import Component from '../src/compiler/Component'; +import Transpiler from '../src/compiler/Transpiler'; +import NodeFS from '@stackpress/types/dist/system/NodeFS'; + +describe('IteratorDirective', () => { + let transpiler: Transpiler; + let component: Component; + + beforeEach(() => { + component = new Component('test.ink', { + fs: new NodeFS(), + cwd: process.cwd() + }); + transpiler = new Transpiler(component); + }); + + // Helper function to create a mock next directive function + const mockNext: NextDirective = (parent: MarkupToken|null, token: MarkupChildToken[], components: Component[]) => { + return 'mockContent'; + }; + + // Helper function to create a base markup token with attributes + const createMarkupToken = (attributes: any): MarkupToken => ({ + type: 'MarkupExpression', + name: 'each', + kind: 'block', + start: 0, + end: 0, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 0, + properties: attributes, + }, + children: [], + }); + + it('should handle array iteration with key and value', () => { + const directive = new IteratorDirective(transpiler); + const token = createMarkupToken([ + { + key: { type: 'Identifier', name: 'value' }, + value: { type: 'Identifier', name: 'item' } + }, + { + key: { type: 'Identifier', name: 'key' }, + value: { type: 'Identifier', name: 'index' } + }, + { + key: { type: 'Identifier', name: 'from' }, + value: { + type: 'ArrayExpression', + start: 0, + end: 0, + elements: [ + { type: 'Literal', value: 'a', start: 0, end: 0, raw: "'a'", escape: false }, + { type: 'Literal', value: 'b', start: 0, end: 0, raw: "'b'", escape: false }, + { type: 'Literal', value: 'c', start: 0, end: 0, raw: "'c'", escape: false } + ] + } + } + ]); + + const result = directive.markup(null, token, [component], mockNext); + expect(result).to.include('Object.entries'); + expect(result).to.include('[index, item]'); + }); + + it('should handle object iteration', () => { + const directive = new IteratorDirective(transpiler); + const token = createMarkupToken([ + { + key: { type: 'Identifier', name: 'value' }, + value: { type: 'Identifier', name: 'val' } + }, + { + key: { type: 'Identifier', name: 'key' }, + value: { type: 'Identifier', name: 'k' } + }, + { + key: { type: 'Identifier', name: 'from' }, + value: { + type: 'ObjectExpression', + start: 0, + end: 0, + properties: [ + { + type: 'Property', + kind: 'init', + start: 0, + end: 0, + key: { type: 'Identifier', name: 'a', start: 0, end: 0 }, + value: { type: 'Literal', value: 1, start: 0, end: 0, raw: "1", escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + }, + { + type: 'Property', + kind: 'init', + start: 0, + end: 0, + key: { type: 'Identifier', name: 'b', start: 0, end: 0 }, + value: { type: 'Literal', value: 2, start: 0, end: 0, raw: "2", escape: false }, + spread: false, + method: false, + shorthand: false, + computed: false + } + ] + } + } + ]); + + const result = directive.markup(null, token, [component], mockNext); + expect(result).to.include('Object.entries'); + expect(result).to.include('[k, val]'); + }); + + it('should handle program expression', () => { + const directive = new IteratorDirective(transpiler); + const token = createMarkupToken([ + { + key: { type: 'Identifier', name: 'value' }, + value: { type: 'Identifier', name: 'item' } + }, + { + key: { type: 'Identifier', name: 'from' }, + value: { + type: 'ProgramExpression', + start: 0, + end: 0, + source: 'items.filter(x => x > 0)', + inline: false, + runtime: false + } + } + ]); + + const result = directive.markup(null, token, [component], mockNext); + expect(result).to.include('items.filter(x => x > 0)'); + }); + + it('should throw error for invalid each statement', () => { + const directive = new IteratorDirective(transpiler); + const token = createMarkupToken([]); + + expect(() => { + directive.markup(null, token, [component], mockNext); + }).to.throw('Invalid each statement'); + }); + + it('should throw error for invalid key type', () => { + const directive = new IteratorDirective(transpiler); + const token = createMarkupToken([ + { + key: { type: 'Identifier', name: 'key' }, + value: { type: 'Literal', value: 'invalid', start: 0, end: 0, raw: "'invalid'", escape: false } + }, + { + key: { type: 'Identifier', name: 'from' }, + value: { type: 'ArrayExpression', start: 0, end: 0, elements: [] } + } + ]); + + expect(() => { + directive.markup(null, token, [component], mockNext); + }).to.throw('Invalid key value in each'); + }); + + it('should throw error for invalid value type', () => { + const directive = new IteratorDirective(transpiler); + const token = createMarkupToken([ + { + key: { type: 'Identifier', name: 'value' }, + value: { type: 'Literal', value: 'invalid', start: 0, end: 0, raw: "'invalid'", escape: false } + }, + { + key: { type: 'Identifier', name: 'from' }, + value: { type: 'ArrayExpression', start: 0, end: 0, elements: [] } + } + ]); + + expect(() => { + directive.markup(null, token, [component], mockNext); + }).to.throw('Invalid value in each'); + }); +}); \ No newline at end of file From dbcb6bee38f3027e7a3599440361788032b23882 Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Thu, 5 Dec 2024 14:00:41 +0800 Subject: [PATCH 07/12] Add test file TryCatchDirective.test.ts for ink --- packages/ink/tests/TryCatchDirective.test.ts | 251 +++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 packages/ink/tests/TryCatchDirective.test.ts diff --git a/packages/ink/tests/TryCatchDirective.test.ts b/packages/ink/tests/TryCatchDirective.test.ts new file mode 100644 index 0000000..1200108 --- /dev/null +++ b/packages/ink/tests/TryCatchDirective.test.ts @@ -0,0 +1,251 @@ +import { TryDirective, CatchDirective } from '../src/directives/TryCatchDirective'; +import Component from '../src/compiler/Component'; +import { MarkupToken, MarkupChildToken, NextDirective } from '../src/types'; +import Exception from '../src/Exception'; +import { expect } from 'chai'; +import Transpiler from '../src/compiler/Transpiler'; + +describe('TryCatchDirective', () => { + let tryDirective: TryDirective; + let catchDirective: CatchDirective; + let mockTranspiler: Transpiler; + let mockNext: NextDirective; + + beforeEach(() => { + // Create a mock component + const mockComponent = new Component('test.ink'); + + // Create a minimal transpiler instance with the mock component + mockTranspiler = new Transpiler(mockComponent); + + // Initialize directives with transpiler + tryDirective = new TryDirective(mockTranspiler); + catchDirective = new CatchDirective(mockTranspiler); + + // Mock the next function with proper NextDirective type + mockNext = (parent: MarkupToken | null, token: MarkupChildToken[], components: Component[]) => 'mockContent'; + }); + + describe('TryDirective', () => { + it('should have the correct name', () => { + expect(tryDirective.name).to.equal('try'); + }); + + it('should throw error if no children are provided', () => { + // Create a minimal token with required properties + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'try', + kind: 'block', + start: 0, + end: 0, + children: undefined // Test undefined children + }; + + expect(() => { + tryDirective.markup(null, token, [], mockNext); + }).to.throw('Invalid try statement'); + }); + + it('should throw error if no catch block is found', () => { + // Create a token with a child but no catch block + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'try', + kind: 'block', + start: 0, + end: 0, + children: [{ + type: 'MarkupExpression', + name: 'div', + kind: 'block', + start: 0, + end: 0 + }] + }; + + expect(() => { + tryDirective.markup(null, token, [], mockNext); + }).to.throw('Invalid try statement'); + }); + + it('should generate correct try block with catch', () => { + // Create a token with a catch block + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'try', + kind: 'block', + start: 0, + end: 0, + children: [{ + type: 'MarkupExpression', + name: 'catch', + kind: 'block', + start: 0, + end: 0 + }] + }; + + const result = tryDirective.markup(null, token, [], mockNext); + expect(result).to.equal('...(() => { try { return mockContent; } })()'); + }); + }); + + describe('CatchDirective', () => { + it('should have the correct name', () => { + expect(catchDirective.name).to.equal('catch'); + }); + + it('should throw error if not within try block', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'catch', + kind: 'block', + start: 0, + end: 0 + }; + + expect(() => { + catchDirective.markup(null, token); + }).to.throw('Invalid catch statement'); + }); + + it('should use default error name if not specified', () => { + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'catch', + kind: 'block', + start: 0, + end: 0 + }; + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'try', + kind: 'block', + start: 0, + end: 0 + }; + + const result = catchDirective.markup(parent, token); + expect(result).to.equal(']; } catch (error) { return ['); + }); + + it('should use custom error name when specified', () => { + // Create a token with custom error name attribute + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'catch', + kind: 'block', + start: 0, + end: 0, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 0, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 0, + key: { + type: 'Identifier', + name: 'error', + start: 0, + end: 0 + }, + value: { + type: 'Identifier', + name: 'e', + start: 0, + end: 0 + }, + spread: false, + method: false, + shorthand: false, + computed: false + }] + } + }; + const parent: MarkupToken = { + type: 'MarkupExpression', + name: 'try', + kind: 'block', + start: 0, + end: 0 + }; + + const result = catchDirective.markup(parent, token); + expect(result).to.equal(']; } catch (e) { return ['); + }); + }); + + // Integration tests + describe('Integration', () => { + it('should handle nested components within try-catch', () => { + // Create a complex token with nested components + const token: MarkupToken = { + type: 'MarkupExpression', + name: 'try', + kind: 'block', + start: 0, + end: 0, + children: [ + { + type: 'MarkupExpression', + name: 'div', + kind: 'block', + start: 0, + end: 0 + }, + { + type: 'MarkupExpression', + name: 'catch', + kind: 'block', + start: 0, + end: 0, + attributes: { + type: 'ObjectExpression', + start: 0, + end: 0, + properties: [{ + type: 'Property', + kind: 'init', + start: 0, + end: 0, + key: { + type: 'Identifier', + name: 'error', + start: 0, + end: 0 + }, + value: { + type: 'Identifier', + name: 'e', + start: 0, + end: 0 + }, + spread: false, + method: false, + shorthand: false, + computed: false + }] + } + } + ] + }; + + // Test the complete try-catch flow + const result = tryDirective.markup(null, token, [], mockNext); + expect(result).to.contain('try { return mockContent'); + + // Verify next function is called + let nextCalled = false; + const mockNextWithTracking: NextDirective = (parent, children, components) => { + nextCalled = true; + return 'mockContent'; + }; + tryDirective.markup(null, token, [], mockNextWithTracking); + expect(nextCalled).to.be.true; + }); + }); +}); \ No newline at end of file From 561a03abadb4f3e32cdb321d898171bf525b4d70 Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Thu, 5 Dec 2024 14:23:20 +0800 Subject: [PATCH 08/12] Add test file DocumentException.test.ts for ink --- packages/ink/tests/DocumentException.test.ts | 197 +++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 packages/ink/tests/DocumentException.test.ts diff --git a/packages/ink/tests/DocumentException.test.ts b/packages/ink/tests/DocumentException.test.ts new file mode 100644 index 0000000..0d6b04e --- /dev/null +++ b/packages/ink/tests/DocumentException.test.ts @@ -0,0 +1,197 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import path from 'node:path'; +import DocumentException from '../src/document/Exception'; +import type { FileSystem } from '@stackpress/types/dist/types'; +import NodeFS from '@stackpress/types/dist/system/NodeFS'; + +describe('DocumentException', () => { + let sandbox: sinon.SinonSandbox; + let errorStub: Error; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + errorStub = new Error('test'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + // Mock FileSystem implementation with only required methods + const mockFS: FileSystem = { + existsSync: () => false, + readFileSync: () => '', + realpathSync: (path: string) => path, + lstatSync: () => ({} as any), + writeFileSync: () => {}, + mkdirSync: () => {}, + createReadStream: () => ({} as any), + unlinkSync: () => {} + }; + + describe('Basic Functionality', () => { + it('should create exception with message', () => { + const message = 'Test error message'; + const exception = new DocumentException(message); + expect(exception.message).to.equal(message); + }); + + it('should extend InkException', () => { + const exception = new DocumentException('test'); + expect(exception).to.be.instanceOf(DocumentException); + }); + }); + + describe('Source Code Handling', () => { + it('should set and return source code', () => { + const source = 'const x = 1;'; + const exception = new DocumentException('test') + .withSource(source); + + // Mock the stack trace using a separate error instance + sandbox.stub(errorStub, 'stack').get(function() { + return `Error: test + at someMethod (${path.sep}test.ts:1:1)`; + }); + sandbox.stub(exception, 'stack').get(() => errorStub.stack); + + const trace = exception.trace(); + expect(trace[0].source).to.equal(source); + }); + + it('should handle empty source code', () => { + const exception = new DocumentException('test') + .withSource(''); + + // Mock the stack trace using a separate error instance + sandbox.stub(errorStub, 'stack').get(function() { + return `Error: test + at someMethod (${path.sep}test.ts:1:1)`; + }); + sandbox.stub(exception, 'stack').get(() => errorStub.stack); + + const trace = exception.trace(); + expect(trace[0].source).to.equal(''); + }); + }); + + describe('FileSystem Integration', () => { + it('should set custom filesystem', () => { + const testFile = path.sep + 'test.ts'; + const existsSyncStub = sandbox.stub().returns(true); + const readFileSyncStub = sandbox.stub().returns('file content'); + const fs = { + ...mockFS, + existsSync: existsSyncStub, + readFileSync: readFileSyncStub + }; + + // Mock the stack trace using a separate error instance + sandbox.stub(errorStub, 'stack').get(function() { + return `Error: test + at evalmachine. (${testFile}:1:1)`; + }); + + const exception = new DocumentException('test') + .withFS(fs); + sandbox.stub(exception, 'stack').get(() => errorStub.stack); + + const trace = exception.trace(); + expect(existsSyncStub.calledWith(testFile)).to.be.true; + expect(readFileSyncStub.calledWith(testFile, 'utf8')).to.be.true; + expect(trace[0].source).to.equal('file content'); + }); + + it('should read source from filesystem for eval traces', () => { + const testFile = path.sep + 'test.ts'; + const fileContent = 'file content'; + + // Create stubs with predefined behavior + const existsSyncStub = sandbox.stub().returns(true); + const readFileSyncStub = sandbox.stub().returns(fileContent); + const fs = { + ...mockFS, + existsSync: existsSyncStub, + readFileSync: readFileSyncStub + }; + + // Mock stack trace using a separate error instance + sandbox.stub(errorStub, 'stack').get(function() { + return `Error: test + at evalmachine. (${testFile}:1:1)`; + }); + + const exception = new DocumentException('test') + .withFS(fs); + sandbox.stub(exception, 'stack').get(() => errorStub.stack); + + const trace = exception.trace(); + expect(trace[0].source).to.equal(fileContent); + expect(readFileSyncStub.calledWith(testFile, 'utf8')).to.be.true; + }); + + it('should handle non-existent files', () => { + const testFile = path.sep + 'nonexistent.ts'; + + const existsSyncStub = sandbox.stub().returns(false); + const readFileSyncStub = sandbox.stub(); + const fs = { + ...mockFS, + existsSync: existsSyncStub, + readFileSync: readFileSyncStub + }; + + // Mock stack trace using a separate error instance + sandbox.stub(errorStub, 'stack').get(function() { + return `Error: test + at evalmachine. (${testFile}:1:1)`; + }); + + const exception = new DocumentException('test') + .withFS(fs); + sandbox.stub(exception, 'stack').get(() => errorStub.stack); + + const trace = exception.trace(); + expect(trace[0].source).to.equal(''); + expect(readFileSyncStub.called).to.be.false; + }); + }); + + describe('Method Chaining', () => { + it('should support method chaining', () => { + const source = 'test source'; + const exception = new DocumentException('test') + .withFS(mockFS) + .withSource(source); + + // Mock the stack trace using a separate error instance + sandbox.stub(errorStub, 'stack').get(function() { + return `Error: test + at someMethod (${path.sep}test.ts:1:1)`; + }); + sandbox.stub(exception, 'stack').get(() => errorStub.stack); + + const trace = exception.trace(); + expect(trace[0].source).to.equal(source); + }); + }); + + describe('Trace Line Numbers', () => { + it('should handle trace with line numbers', () => { + const source = 'test source'; + const exception = new DocumentException('test') + .withSource(source); + + // Mock the stack trace using a separate error instance + sandbox.stub(errorStub, 'stack').get(function() { + return `Error: test + at someMethod (${path.sep}test.ts:1:1)`; + }); + sandbox.stub(exception, 'stack').get(() => errorStub.stack); + + const trace = exception.trace(1, 5); + expect(trace[0].source).to.equal(source); + }); + }); +}); \ No newline at end of file From ac0cac31f9b81aad252ece766158ff0596c6460d Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Thu, 5 Dec 2024 23:56:23 +0800 Subject: [PATCH 09/12] Add tests to DocumentBuilder.test.ts in ink --- packages/ink/tests/DocumentBuilder.test.ts | 245 +++++++++++++++++++-- 1 file changed, 222 insertions(+), 23 deletions(-) diff --git a/packages/ink/tests/DocumentBuilder.test.ts b/packages/ink/tests/DocumentBuilder.test.ts index 7526251..f9ab4d3 100644 --- a/packages/ink/tests/DocumentBuilder.test.ts +++ b/packages/ink/tests/DocumentBuilder.test.ts @@ -1,32 +1,231 @@ import path from 'path'; -import { describe, it } from 'mocha'; +import { describe, it, beforeEach } from 'mocha'; import { expect } from 'chai'; - +import EventEmitter from '../src/EventEmitter'; import Component from '../src/compiler/Component'; import Builder from '../src/document/Builder'; describe('Ink Document Builder', () => { - //determine the tsconfig path const tsconfig = path.join(__dirname, '../tsconfig.json'); - //make a document component - const component = new Component( - path.join(__dirname, 'fixtures/page.ink'), - { cwd: __dirname } - ); - it('Should build document', async () => { - const builder = new Builder(component, { tsconfig, minify: false }); - const server = await builder.server(); - //console.log('server', server) - expect(server).to.contain('...this._toNodeList(snippet1)'); - expect(server).to.contain('...this._toNodeList(snippet2)'); - const client = await builder.client(); - //console.log('client', client) - expect(client).to.contain('const snippet1 = `//on server:'); - expect(client).to.contain('const snippet2 = ` From a202e45d9d575b3958600cfa9fc88399aef6c566 Mon Sep 17 00:00:00 2001 From: amerah-abdul Date: Fri, 6 Dec 2024 15:48:11 +0800 Subject: [PATCH 12/12] addressed requested changes --- packages/ink-css/package.json | 15 +++++++-------- packages/ink/package.json | 19 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/ink-css/package.json b/packages/ink-css/package.json index 736297c..e0f2595 100644 --- a/packages/ink-css/package.json +++ b/packages/ink-css/package.json @@ -21,17 +21,16 @@ "test": "nyc ts-mocha tests/*.test.ts" }, "dependencies": { - "@stackpress/ink": "0.3.6" + "@stackpress/ink": "0.3.10" }, "devDependencies": { - "@types/chai":"5.0.1", + "@types/chai": "4.3.20", "@types/mocha": "10.0.10", "@types/node": "22.9.3", - "chai":"5.1.2", - "mocha":"11.0.0", - "nyc":"17.1.0", - "ts-mocha":"10.0.0", - "ts-node": "10.9.2", - "typescript": "5.7.2" + "chai": "4.5.0", + "mocha": "10.8.2", + "nyc": "17.1.0", + "ts-mocha": "10.0.0", + "ts-node": "10.9.2" } } \ No newline at end of file diff --git a/packages/ink/package.json b/packages/ink/package.json index 580df3c..b283d46 100644 --- a/packages/ink/package.json +++ b/packages/ink/package.json @@ -28,25 +28,24 @@ "README.md", "tsconfig.json" ], - "scripts": { + "scripts": { "build": "tsc", "test": "nyc ts-mocha tests/*.test.ts" }, "dependencies": { - "@stackpress/types": "0.3.6", + "@stackpress/types": "0.3.10", "esbuild": "0.24.0", "ts-morph": "24.0.0", "typescript": "5.7.2" }, "devDependencies": { - "@types/chai":"5.0.1", + "@types/chai": "4.3.20", "@types/mocha": "10.0.10", "@types/node": "22.9.3", - "chai":"5.1.2", - "mocha":"11.0.0", - "nyc":"17.1.0", - "ts-mocha":"10.0.0", - "ts-node": "10.9.2", - "typescript": "5.7.2" + "chai": "4.5.0", + "mocha": "10.8.2", + "nyc": "17.1.0", + "ts-mocha": "10.0.0", + "ts-node": "10.9.2" } -} +} \ No newline at end of file