From 0f77e16524c1914d56b8a6586c92b6d3cd372424 Mon Sep 17 00:00:00 2001 From: Sagarika Dasgupta Date: Wed, 22 Oct 2025 13:14:48 -0500 Subject: [PATCH 1/8] Using function runner directly --- package.json | 1 + src/methods/run-function.ts | 301 ++++++++++++++++-- .../cart-validation-js/tests/default.test.js | 13 +- .../tests/fixtures/cda6d1.json | 36 +-- .../discount-function/tests/default.test.js | 13 +- test/methods/run-function.test.ts | 41 ++- 6 files changed, 325 insertions(+), 80 deletions(-) diff --git a/package.json b/package.json index 6625b82..30069c1 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "prepublishOnly": "pnpm run build && pnpm run test && pnpm run lint" }, "dependencies": { + "@iarna/toml": "^2.2.5", "graphql": "^16.11.0" }, "devDependencies": { diff --git a/src/methods/run-function.ts b/src/methods/run-function.ts index e38e5cc..595c99c 100644 --- a/src/methods/run-function.ts +++ b/src/methods/run-function.ts @@ -4,7 +4,9 @@ import { spawn } from 'child_process'; import path from 'path'; -import { fileURLToPath } from 'url'; +import fs from 'fs'; +import { parse as parseToml } from '@iarna/toml'; +import { FixtureData } from './load-fixture.js'; /** * Interface for the run function result @@ -12,46 +14,37 @@ import { fileURLToPath } from 'url'; export interface RunFunctionResult { result: { output: any } | null; error: string | null; + timing?: { + startTime: number; + endTime: number; + durationMs: number; + }; } -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - /** - * Run a function with the given payload and return the result - * @param {String} exportName - The function run payload - * @param {String} input - The actual function implementation to test - * @param {String} [functionPath] - Optional path to the function directory + * Run a function with the given fixture and return the result + * @param {FixtureData} fixture - The fixture data containing export and input + * @param {String} functionPath - Path to the function directory * @returns {Object} The function run result */ export async function runFunction( - exportName: string, - input: Record, - functionPath?: string + fixture: FixtureData, + functionPath: string ): Promise { + const startTime = Date.now(); + try { - const inputJson = JSON.stringify(input); - - let functionDir, appRootDir, functionName; - - if (functionPath !== undefined && functionPath !== null) { - // Use provided function path - functionDir = path.resolve(functionPath); - appRootDir = path.dirname(functionDir); - functionName = path.basename(functionDir); - } else { - // Calculate paths correctly for when used as a dependency: - // __dirname = /path/to/function/tests/node_modules/function-testing-helpers/src/methods - // Go up 5 levels to get to function directory: ../../../../../ = /path/to/function - functionDir = path.dirname(path.dirname(path.dirname(path.dirname(path.dirname(__dirname))))); - appRootDir = path.dirname(functionDir); - functionName = path.basename(functionDir); - } - + const inputJson = JSON.stringify(fixture.input); + + // Use provided function path + const functionDir = path.resolve(functionPath); + const appRootDir = path.dirname(functionDir); + const functionName = path.basename(functionDir); + return new Promise((resolve, reject) => { const shopifyProcess = spawn('shopify', [ 'app', 'function', 'run', - '--export', exportName, + '--export', fixture.export, '--json', '--path', functionName ], { @@ -71,10 +64,18 @@ export async function runFunction( }); shopifyProcess.on('close', (code) => { + const endTime = Date.now(); + const timing = { + startTime, + endTime, + durationMs: endTime - startTime + }; + if (code !== 0) { resolve({ result: null, error: `Command failed with exit code ${code}: ${stderr}`, + timing }); return; } @@ -95,19 +96,27 @@ export async function runFunction( resolve({ result: { output: actualOutput }, error: null, + timing }); } catch (parseError) { resolve({ result: { output: stdout.trim() }, error: null, + timing }); } }); shopifyProcess.on('error', (error) => { + const endTime = Date.now(); resolve({ result: null, error: `Failed to start shopify command: ${error.message}`, + timing: { + startTime, + endTime, + durationMs: endTime - startTime + } }); }); @@ -116,17 +125,249 @@ export async function runFunction( }); } catch (error) { + const endTime = Date.now(); + if (error instanceof Error) { + return { + result: null, + error: error.message, + timing: { + startTime, + endTime, + durationMs: endTime - startTime + } + }; + } else { + return { + result: null, + error: 'Unknown error occurred', + timing: { + startTime, + endTime, + durationMs: endTime - startTime + } + }; + } + } +} + +/** + * Run a function using Shopify CLI's function runner command + * + * This function: + * - Uses `shopify app function runner` which wraps function-runner + * - Parses shopify.extension.toml to find the extension matching the fixture's target + * - Extracts the WASM build path and input_query path from the matching extension + * - Falls back to common locations if TOML parsing fails or no match is found + * - Typically faster than runFunction() as it bypasses function build/run orchestration + * + * @param {FixtureData} fixture - The fixture data containing export, input, and target + * @param {String} functionPath - Path to the function directory + * @returns {Object} The function run result + */ + +export async function runFunctionRunner( + fixture: FixtureData, + functionPath: string +): Promise { + const startTime = Date.now(); + + try { + const inputJson = JSON.stringify(fixture.input); + + // Use provided function path + const functionDir = path.resolve(functionPath); + const functionName = path.basename(functionDir); + const appRootDir = path.dirname(functionDir); + + // Parse shopify.extension.toml once to get all needed values + let resolvedWasmPath: string | null = null; + let schemaPath = path.join(functionDir, 'schema.graphql'); + let queryPath = path.join(functionDir, 'src', `${fixture.export}.graphql`); + + // Try to read from shopify.extension.toml and match by target + const tomlPath = path.join(functionDir, 'shopify.extension.toml'); + if (fs.existsSync(tomlPath)) { + try { + const tomlContent = fs.readFileSync(tomlPath, 'utf-8'); + const config = parseToml(tomlContent) as any; + + // Look for extensions with matching target + if (config.extensions && Array.isArray(config.extensions)) { + for (const ext of config.extensions) { + // Check if this extension has a targeting section that matches the fixture target + if (ext.targeting && Array.isArray(ext.targeting)) { + for (const targeting of ext.targeting) { + if (targeting.target === fixture.target) { + // Found matching target, extract all needed paths + if (ext.build && ext.build.path) { + const tomlWasmPath = path.join(functionDir, ext.build.path); + if (fs.existsSync(tomlWasmPath)) { + resolvedWasmPath = tomlWasmPath; + } + } + + // Get input_query path from targeting + if (targeting.input_query) { + queryPath = path.join(functionDir, targeting.input_query); + } + + break; + } + } + } + + if (resolvedWasmPath) break; + } + + // If no matching target found, fall back to first extension with build.path + if (!resolvedWasmPath) { + for (const ext of config.extensions) { + if (ext.build && ext.build.path) { + const tomlWasmPath = path.join(functionDir, ext.build.path); + if (fs.existsSync(tomlWasmPath)) { + resolvedWasmPath = tomlWasmPath; + break; + } + } + } + } + } + } catch (tomlError) { + // If TOML parsing fails, fall through to default search + } + } + + // If not found in TOML, check common locations + if (!resolvedWasmPath) { + // Check Rust build output location + const rustWasmPath = path.join(functionDir, 'target', 'wasm32-wasip1', 'release', `${functionName}.wasm`); + if (fs.existsSync(rustWasmPath)) { + resolvedWasmPath = rustWasmPath; + } else { + // Check dist folder (JavaScript functions) + const distWasmPath = path.join(functionDir, 'dist', 'function.wasm'); + if (fs.existsSync(distWasmPath)) { + resolvedWasmPath = distWasmPath; + } + } + } + + if (!resolvedWasmPath) { + const endTime = Date.now(); + return { + result: null, + error: `WASM file not found in function directory: ${functionDir}.`, + timing: { + startTime, + endTime, + durationMs: endTime - startTime + } + }; + } + + return new Promise((resolve) => { + const runnerProcess = spawn('shopify', [ + 'app', 'function', 'runner', + '--wasm-path', resolvedWasmPath, + '--export', fixture.export, + '--json', + ], { + cwd: appRootDir, + stdio: ['pipe', 'pipe', 'pipe'], + env: { + ...process.env, + SHOPIFY_INVOKED_BY: 'shopify-function-test-helpers' + } + }); + + let stdout = ''; + let stderr = ''; + + runnerProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + runnerProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + runnerProcess.on('close', (code) => { + const endTime = Date.now(); + const timing = { + startTime, + endTime, + durationMs: endTime - startTime + }; + + if (code !== 0) { + resolve({ + result: null, + error: `function-runner failed with exit code ${code}: ${stderr}`, + timing + }); + return; + } + + try { + const result = JSON.parse(stdout); + + // function-runner output format: { output: {...} } + resolve({ + result: { output: result.output || result }, + error: null, + timing + }); + } catch (parseError) { + resolve({ + result: null, + error: `Failed to parse function-runner output: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`, + timing + }); + } + }); + + runnerProcess.on('error', (error) => { + const endTime = Date.now(); + resolve({ + result: null, + error: `Failed to start function-runner: ${error.message}`, + timing: { + startTime, + endTime, + durationMs: endTime - startTime + } + }); + }); + + runnerProcess.stdin.write(inputJson); + runnerProcess.stdin.end(); + }); + + } catch (error) { + const endTime = Date.now(); if (error instanceof Error) { return { result: null, error: error.message, + timing: { + startTime, + endTime, + durationMs: endTime - startTime + } }; } else { return { result: null, error: 'Unknown error occurred', + timing: { + startTime, + endTime, + durationMs: endTime - startTime + } }; } } } + + diff --git a/test-app/extensions/cart-validation-js/tests/default.test.js b/test-app/extensions/cart-validation-js/tests/default.test.js index aa39873..64e83f6 100644 --- a/test-app/extensions/cart-validation-js/tests/default.test.js +++ b/test-app/extensions/cart-validation-js/tests/default.test.js @@ -1,6 +1,6 @@ import path from "path"; import fs from "fs"; -import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery } from "@shopify/shopify-function-test-helpers"; +import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery, runFunctionRunner } from "@shopify/shopify-function-test-helpers"; describe("Default Integration Test", () => { let schema; @@ -36,24 +36,21 @@ describe("Default Integration Test", () => { inputQueryAST }); - // Log validation results for debugging - // logValidationResults(fixtureFile, validationResult); - // Assert that all validation steps pass expect(validationResult.inputQuery.errors).toHaveLength(0); expect(validationResult.inputFixture.errors).toHaveLength(0); expect(validationResult.outputFixture.errors).toHaveLength(0); // Run the actual function - const runResult = await runFunction( - fixture.export, - fixture.input, + const runResult = await runFunctionRunner( + fixture, functionDir ); - const { result, error } = runResult; + const { result, error, timing } = runResult; expect(error).toBeNull(); expect(result.output).toEqual(fixture.expectedOutput); + console.log(timing ? `Execution time: ${timing.durationMs}ms` : 'No timing information available'); }, 10000); }); }); \ No newline at end of file diff --git a/test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json b/test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json index fbd8f93..715e98c 100644 --- a/test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json +++ b/test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json @@ -1,24 +1,24 @@ { "payload": { "export": "cart-validations-generate-run", - "target": "cart.validations.generate.run", - "input": { - "cart": { - "lines": [ - { - "quantity": 1 - }, - { - "quantity": 1 - }, - { - "quantity": 1 - } - ] + "target": "cart.validations.generate.run", + "input": { + "cart": { + "lines": [ + { + "quantity": 1 + }, + { + "quantity": 1 + }, + { + "quantity": 1 + } + ] + } + }, + "output": { + "operations": [{ "validationAdd": { "errors": [] } }] } - }, - "output": { - "operations": [{ "validationAdd": { "errors": [] } }] - } } } \ No newline at end of file diff --git a/test-app/extensions/discount-function/tests/default.test.js b/test-app/extensions/discount-function/tests/default.test.js index 57b842f..4b97e40 100644 --- a/test-app/extensions/discount-function/tests/default.test.js +++ b/test-app/extensions/discount-function/tests/default.test.js @@ -1,6 +1,6 @@ import path from "path"; import fs from "fs"; -import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery } from "@shopify/shopify-function-test-helpers"; +import { buildFunction, loadFixture, runFunctionRunner, validateTestAssets, loadSchema, loadInputQuery } from "@shopify/shopify-function-test-helpers"; describe("Default Integration Test", () => { let schema; @@ -36,24 +36,21 @@ describe("Default Integration Test", () => { inputQueryAST }); - // Log validation results for debugging - // logValidationResults(fixtureFile, validationResult); - // Assert that all validation steps pass expect(validationResult.inputQuery.errors).toHaveLength(0); expect(validationResult.inputFixture.errors).toHaveLength(0); expect(validationResult.outputFixture.errors).toHaveLength(0); // Run the actual function - const runResult = await runFunction( - fixture.export, - fixture.input, + const runResult = await runFunctionRunner( + fixture, functionDir ); - const { result, error } = runResult; + const { result, error, timing } = runResult; expect(error).toBeNull(); expect(result.output).toEqual(fixture.expectedOutput); + console.log(timing ? `Execution time: ${timing.durationMs}ms` : 'No timing information available'); }, 10000); }); }); \ No newline at end of file diff --git a/test/methods/run-function.test.ts b/test/methods/run-function.test.ts index a994680..87eec64 100644 --- a/test/methods/run-function.test.ts +++ b/test/methods/run-function.test.ts @@ -1,29 +1,38 @@ import { describe, it, expect } from 'vitest'; import { runFunction } from '../../src/methods/run-function.ts'; import { loadFixture } from '../../src/methods/load-fixture.ts'; +import { FixtureData } from '../../src/methods/load-fixture.ts'; describe('runFunction', () => { it('should run a function using Shopify CLI', async () => { - const exportName = 'cart-validations-generate-run'; - const input = { - cart: { - lines: [{ quantity: 1 }] - } + const fixture: FixtureData = { + export: 'cart-validations-generate-run', + input: { + cart: { + lines: [{ quantity: 1 }] + } + }, + expectedOutput: {}, + target: 'cart.validations.generate.run' }; - - const result = await runFunction(exportName, input, 'test-app/extensions/cart-validation-js'); - + + const result = await runFunction(fixture, 'test-app/extensions/cart-validation-js'); + expect(result).toBeDefined(); expect(result).toHaveProperty('result'); expect(result).toHaveProperty('error'); }); it('should handle function execution errors gracefully', async () => { - const exportName = 'invalid_export'; - const input = { cart: { lines: [] } }; - - const result = await runFunction(exportName, input, 'test-app/extensions/cart-validation-js'); - + const fixture: FixtureData = { + export: 'invalid_export', + input: { cart: { lines: [] } }, + expectedOutput: {}, + target: 'cart.validations.generate.run' + }; + + const result = await runFunction(fixture, 'test-app/extensions/cart-validation-js'); + expect(result).toBeDefined(); expect(result).toHaveProperty('result'); expect(result).toHaveProperty('error'); @@ -31,9 +40,9 @@ describe('runFunction', () => { it('should work with fixture data', async () => { const fixture = await loadFixture('test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json'); - - const result = await runFunction(fixture.export, fixture.input, 'test-app/extensions/cart-validation-js'); - + + const result = await runFunction(fixture, 'test-app/extensions/cart-validation-js'); + expect(result).toBeDefined(); expect(result).toHaveProperty('result'); expect(result).toHaveProperty('error'); From fe12442229eeeccc121ee12331fccf22c02fdbfd Mon Sep 17 00:00:00 2001 From: Sagarika Dasgupta Date: Thu, 23 Oct 2025 14:41:14 -0500 Subject: [PATCH 2/8] using info command to get relevant info from CLI --- package.json | 1 - src/methods/run-function.ts | 299 ++-------------- .../cart-validation-js/tests/default.test.js | 52 ++- ...=> checkout-validation-valid-fixture.json} | 0 .../discount-function/tests/default.test.js | 50 ++- test/methods/load-fixture.test.ts | 2 +- test/methods/run-function.test.ts | 319 ++++++++++++++++-- 7 files changed, 385 insertions(+), 338 deletions(-) rename test-app/extensions/cart-validation-js/tests/fixtures/{cda6d1.json => checkout-validation-valid-fixture.json} (100%) diff --git a/package.json b/package.json index 30069c1..6625b82 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "prepublishOnly": "pnpm run build && pnpm run test && pnpm run lint" }, "dependencies": { - "@iarna/toml": "^2.2.5", "graphql": "^16.11.0" }, "devDependencies": { diff --git a/src/methods/run-function.ts b/src/methods/run-function.ts index 595c99c..c2276be 100644 --- a/src/methods/run-function.ts +++ b/src/methods/run-function.ts @@ -3,9 +3,6 @@ */ import { spawn } from 'child_process'; -import path from 'path'; -import fs from 'fs'; -import { parse as parseToml } from '@iarna/toml'; import { FixtureData } from './load-fixture.js'; /** @@ -14,270 +11,40 @@ import { FixtureData } from './load-fixture.js'; export interface RunFunctionResult { result: { output: any } | null; error: string | null; - timing?: { - startTime: number; - endTime: number; - durationMs: number; - }; -} - -/** - * Run a function with the given fixture and return the result - * @param {FixtureData} fixture - The fixture data containing export and input - * @param {String} functionPath - Path to the function directory - * @returns {Object} The function run result - */ -export async function runFunction( - fixture: FixtureData, - functionPath: string -): Promise { - const startTime = Date.now(); - - try { - const inputJson = JSON.stringify(fixture.input); - - // Use provided function path - const functionDir = path.resolve(functionPath); - const appRootDir = path.dirname(functionDir); - const functionName = path.basename(functionDir); - - return new Promise((resolve, reject) => { - const shopifyProcess = spawn('shopify', [ - 'app', 'function', 'run', - '--export', fixture.export, - '--json', - '--path', functionName - ], { - cwd: appRootDir, - stdio: ['pipe', 'pipe', 'pipe'] - }); - - let stdout = ''; - let stderr = ''; - - shopifyProcess.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - shopifyProcess.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - shopifyProcess.on('close', (code) => { - const endTime = Date.now(); - const timing = { - startTime, - endTime, - durationMs: endTime - startTime - }; - - if (code !== 0) { - resolve({ - result: null, - error: `Command failed with exit code ${code}: ${stderr}`, - timing - }); - return; - } - - let result; - try { - result = JSON.parse(stdout); - - let actualOutput; - if (result?.output?.humanized) { - actualOutput = JSON.parse(result.output.humanized); - } else if (result?.output) { - actualOutput = result.output; - } else { - actualOutput = result; - } - - resolve({ - result: { output: actualOutput }, - error: null, - timing - }); - } catch (parseError) { - resolve({ - result: { output: stdout.trim() }, - error: null, - timing - }); - } - }); - - shopifyProcess.on('error', (error) => { - const endTime = Date.now(); - resolve({ - result: null, - error: `Failed to start shopify command: ${error.message}`, - timing: { - startTime, - endTime, - durationMs: endTime - startTime - } - }); - }); - - shopifyProcess.stdin.write(inputJson); - shopifyProcess.stdin.end(); - }); - - } catch (error) { - const endTime = Date.now(); - if (error instanceof Error) { - return { - result: null, - error: error.message, - timing: { - startTime, - endTime, - durationMs: endTime - startTime - } - }; - } else { - return { - result: null, - error: 'Unknown error occurred', - timing: { - startTime, - endTime, - durationMs: endTime - startTime - } - }; - } - } } /** * Run a function using Shopify CLI's function runner command * * This function: - * - Uses `shopify app function runner` which wraps function-runner - * - Parses shopify.extension.toml to find the extension matching the fixture's target - * - Extracts the WASM build path and input_query path from the matching extension - * - Falls back to common locations if TOML parsing fails or no match is found - * - Typically faster than runFunction() as it bypasses function build/run orchestration - * + * - Uses function-runner binary directly to run the function. + * @param {String} functionRunnerPath - Path to the function runner binary + * @param {String} wasmPath - Path to the WASM file * @param {FixtureData} fixture - The fixture data containing export, input, and target - * @param {String} functionPath - Path to the function directory + * @param {String} queryPath - Path to the input query file + * @param {String} schemaPath - Path to the schema file * @returns {Object} The function run result */ -export async function runFunctionRunner( +export async function runFunction( + functionRunnerPath: string, + wasmPath: string, fixture: FixtureData, - functionPath: string + queryPath: string, + schemaPath: string, ): Promise { - const startTime = Date.now(); - try { const inputJson = JSON.stringify(fixture.input); - // Use provided function path - const functionDir = path.resolve(functionPath); - const functionName = path.basename(functionDir); - const appRootDir = path.dirname(functionDir); - - // Parse shopify.extension.toml once to get all needed values - let resolvedWasmPath: string | null = null; - let schemaPath = path.join(functionDir, 'schema.graphql'); - let queryPath = path.join(functionDir, 'src', `${fixture.export}.graphql`); - - // Try to read from shopify.extension.toml and match by target - const tomlPath = path.join(functionDir, 'shopify.extension.toml'); - if (fs.existsSync(tomlPath)) { - try { - const tomlContent = fs.readFileSync(tomlPath, 'utf-8'); - const config = parseToml(tomlContent) as any; - - // Look for extensions with matching target - if (config.extensions && Array.isArray(config.extensions)) { - for (const ext of config.extensions) { - // Check if this extension has a targeting section that matches the fixture target - if (ext.targeting && Array.isArray(ext.targeting)) { - for (const targeting of ext.targeting) { - if (targeting.target === fixture.target) { - // Found matching target, extract all needed paths - if (ext.build && ext.build.path) { - const tomlWasmPath = path.join(functionDir, ext.build.path); - if (fs.existsSync(tomlWasmPath)) { - resolvedWasmPath = tomlWasmPath; - } - } - - // Get input_query path from targeting - if (targeting.input_query) { - queryPath = path.join(functionDir, targeting.input_query); - } - - break; - } - } - } - - if (resolvedWasmPath) break; - } - - // If no matching target found, fall back to first extension with build.path - if (!resolvedWasmPath) { - for (const ext of config.extensions) { - if (ext.build && ext.build.path) { - const tomlWasmPath = path.join(functionDir, ext.build.path); - if (fs.existsSync(tomlWasmPath)) { - resolvedWasmPath = tomlWasmPath; - break; - } - } - } - } - } - } catch (tomlError) { - // If TOML parsing fails, fall through to default search - } - } - - // If not found in TOML, check common locations - if (!resolvedWasmPath) { - // Check Rust build output location - const rustWasmPath = path.join(functionDir, 'target', 'wasm32-wasip1', 'release', `${functionName}.wasm`); - if (fs.existsSync(rustWasmPath)) { - resolvedWasmPath = rustWasmPath; - } else { - // Check dist folder (JavaScript functions) - const distWasmPath = path.join(functionDir, 'dist', 'function.wasm'); - if (fs.existsSync(distWasmPath)) { - resolvedWasmPath = distWasmPath; - } - } - } - - if (!resolvedWasmPath) { - const endTime = Date.now(); - return { - result: null, - error: `WASM file not found in function directory: ${functionDir}.`, - timing: { - startTime, - endTime, - durationMs: endTime - startTime - } - }; - } - return new Promise((resolve) => { - const runnerProcess = spawn('shopify', [ - 'app', 'function', 'runner', - '--wasm-path', resolvedWasmPath, + const runnerProcess = spawn(functionRunnerPath, [ + '-f', wasmPath, '--export', fixture.export, + '--query-path', queryPath, + '--schema-path', schemaPath, '--json', ], { - cwd: appRootDir, stdio: ['pipe', 'pipe', 'pipe'], - env: { - ...process.env, - SHOPIFY_INVOKED_BY: 'shopify-function-test-helpers' - } }); let stdout = ''; @@ -292,18 +59,11 @@ export async function runFunctionRunner( }); runnerProcess.on('close', (code) => { - const endTime = Date.now(); - const timing = { - startTime, - endTime, - durationMs: endTime - startTime - }; if (code !== 0) { resolve({ result: null, - error: `function-runner failed with exit code ${code}: ${stderr}`, - timing + error: `function-runner failed with exit code ${code}: ${stderr}` }); return; } @@ -314,14 +74,12 @@ export async function runFunctionRunner( // function-runner output format: { output: {...} } resolve({ result: { output: result.output || result }, - error: null, - timing + error: null }); } catch (parseError) { resolve({ result: null, - error: `Failed to parse function-runner output: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`, - timing + error: `Failed to parse function-runner output: ${parseError instanceof Error ? parseError.message : 'Unknown error'}` }); } }); @@ -330,12 +88,7 @@ export async function runFunctionRunner( const endTime = Date.now(); resolve({ result: null, - error: `Failed to start function-runner: ${error.message}`, - timing: { - startTime, - endTime, - durationMs: endTime - startTime - } + error: `Failed to start function-runner: ${error.message}` }); }); @@ -344,30 +97,16 @@ export async function runFunctionRunner( }); } catch (error) { - const endTime = Date.now(); if (error instanceof Error) { return { result: null, - error: error.message, - timing: { - startTime, - endTime, - durationMs: endTime - startTime - } + error: error.message }; } else { return { result: null, error: 'Unknown error occurred', - timing: { - startTime, - endTime, - durationMs: endTime - startTime - } }; } } } - - - diff --git a/test-app/extensions/cart-validation-js/tests/default.test.js b/test-app/extensions/cart-validation-js/tests/default.test.js index 64e83f6..c20922e 100644 --- a/test-app/extensions/cart-validation-js/tests/default.test.js +++ b/test-app/extensions/cart-validation-js/tests/default.test.js @@ -1,20 +1,48 @@ import path from "path"; import fs from "fs"; -import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery, runFunctionRunner } from "@shopify/shopify-function-test-helpers"; +import { execSync } from "child_process"; +import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery } from "@shopify/shopify-function-test-helpers"; describe("Default Integration Test", () => { let schema; let inputQueryAST; let functionDir; + let schemaPath; + let inputQueryPath; + let functionRunnerPath; + let wasmPath; beforeAll(async () => { functionDir = path.dirname(__dirname); await buildFunction(functionDir); - - // Load schema and input query once since they don't change across fixtures - const schemaPath = path.join(functionDir, "schema.graphql"); - const inputQueryPath = path.join(functionDir, "src/cart_validations_generate_run.graphql"); - + + // Get function info from Shopify CLI + let functionInfoJson; + try { + functionInfoJson = execSync( + `shopify app function info --json --path ${functionDir}`, + { + encoding: 'utf-8' + } + ); + } catch (error) { + // Check if the error is due to the command not being found + if (error.message.includes('Command app function info not found')) { + throw new Error( + 'The "shopify app function info" command is not available in your CLI version.\n' + + 'Please upgrade to the latest version:\n' + + ' npm install -g @shopify/cli@latest\n\n' + ); + } + throw error; + } + + const functionInfo = JSON.parse(functionInfoJson); + schemaPath = functionInfo.functionSchemaPath; + inputQueryPath = functionInfo.functionInputQueryPath; + functionRunnerPath = functionInfo.functionRunnerPath; + wasmPath = functionInfo.functionWasmPath; + schema = await loadSchema(schemaPath); inputQueryAST = await loadInputQuery(inputQueryPath); }, 20000); // 20 second timeout for building the function @@ -42,15 +70,17 @@ describe("Default Integration Test", () => { expect(validationResult.outputFixture.errors).toHaveLength(0); // Run the actual function - const runResult = await runFunctionRunner( - fixture, - functionDir + const runResult = await runFunction( + functionRunnerPath, + wasmPath, + fixture, + inputQueryPath, + schemaPath ); - const { result, error, timing } = runResult; + const { result, error } = runResult; expect(error).toBeNull(); expect(result.output).toEqual(fixture.expectedOutput); - console.log(timing ? `Execution time: ${timing.durationMs}ms` : 'No timing information available'); }, 10000); }); }); \ No newline at end of file diff --git a/test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json b/test-app/extensions/cart-validation-js/tests/fixtures/checkout-validation-valid-fixture.json similarity index 100% rename from test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json rename to test-app/extensions/cart-validation-js/tests/fixtures/checkout-validation-valid-fixture.json diff --git a/test-app/extensions/discount-function/tests/default.test.js b/test-app/extensions/discount-function/tests/default.test.js index 4b97e40..f8d5577 100644 --- a/test-app/extensions/discount-function/tests/default.test.js +++ b/test-app/extensions/discount-function/tests/default.test.js @@ -1,20 +1,48 @@ import path from "path"; import fs from "fs"; -import { buildFunction, loadFixture, runFunctionRunner, validateTestAssets, loadSchema, loadInputQuery } from "@shopify/shopify-function-test-helpers"; +import { execSync } from "child_process"; +import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery } from "@shopify/shopify-function-test-helpers"; describe("Default Integration Test", () => { let schema; let inputQueryAST; let functionDir; + let schemaPath; + let inputQueryPath; + let functionRunnerPath; + let wasmPath; beforeAll(async () => { functionDir = path.dirname(__dirname); await buildFunction(functionDir); - - // Load schema and input query once since they don't change across fixtures - const schemaPath = path.join(functionDir, "schema.graphql"); - const inputQueryPath = path.join(functionDir, "src/cart_lines_discounts_generate_run.graphql"); - + + // Get function info from Shopify CLI + let functionInfoJson; + try { + functionInfoJson = execSync( + `shopify app function info --json --path ${functionDir}`, + { + encoding: 'utf-8' + } + ); + } catch (error) { + // Check if the error is due to the command not being found + if (error.message.includes('Command app function info not found')) { + throw new Error( + 'The "shopify app function info" command is not available in your CLI version.\n' + + 'Please upgrade to the latest version:\n' + + ' npm install -g @shopify/cli@latest\n\n' + ); + } + throw error; + } + + const functionInfo = JSON.parse(functionInfoJson); + schemaPath = functionInfo.functionSchemaPath; + inputQueryPath = functionInfo.functionInputQueryPath; + functionRunnerPath = functionInfo.functionRunnerPath; + wasmPath = functionInfo.functionWasmPath; + schema = await loadSchema(schemaPath); inputQueryAST = await loadInputQuery(inputQueryPath); }, 20000); // 20 second timeout for building the function @@ -42,15 +70,17 @@ describe("Default Integration Test", () => { expect(validationResult.outputFixture.errors).toHaveLength(0); // Run the actual function - const runResult = await runFunctionRunner( + const runResult = await runFunction( + functionRunnerPath, + wasmPath, fixture, - functionDir + inputQueryPath, + schemaPath ); - const { result, error, timing } = runResult; + const { result, error } = runResult; expect(error).toBeNull(); expect(result.output).toEqual(fixture.expectedOutput); - console.log(timing ? `Execution time: ${timing.durationMs}ms` : 'No timing information available'); }, 10000); }); }); \ No newline at end of file diff --git a/test/methods/load-fixture.test.ts b/test/methods/load-fixture.test.ts index 552dfc1..df7d2c1 100644 --- a/test/methods/load-fixture.test.ts +++ b/test/methods/load-fixture.test.ts @@ -3,7 +3,7 @@ import { loadFixture } from '../../src/methods/load-fixture.ts'; describe('loadFixture', () => { it('should load fixture from a valid JSON file', async () => { - const fixture = await loadFixture('test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json'); + const fixture = await loadFixture('test-app/extensions/cart-validation-js/tests/fixtures/checkout-validation-valid-fixture.json'); expect(fixture).toBeDefined(); expect(fixture.export).toBe('cart-validations-generate-run'); expect(fixture.target).toBe('cart.validations.generate.run'); diff --git a/test/methods/run-function.test.ts b/test/methods/run-function.test.ts index 87eec64..94149c4 100644 --- a/test/methods/run-function.test.ts +++ b/test/methods/run-function.test.ts @@ -1,50 +1,299 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { EventEmitter } from 'events'; +import { Writable } from 'stream'; +import * as childProcess from 'child_process'; import { runFunction } from '../../src/methods/run-function.ts'; -import { loadFixture } from '../../src/methods/load-fixture.ts'; import { FixtureData } from '../../src/methods/load-fixture.ts'; +// Mock child_process +vi.mock('child_process'); + describe('runFunction', () => { - it('should run a function using Shopify CLI', async () => { - const fixture: FixtureData = { - export: 'cart-validations-generate-run', - input: { - cart: { - lines: [{ quantity: 1 }] - } - }, - expectedOutput: {}, - target: 'cart.validations.generate.run' - }; + let mockStdin: Writable & { end: ReturnType }; + let mockStdout: EventEmitter; + let mockStderr: EventEmitter; + let mockProcess: EventEmitter & { + stdin: typeof mockStdin; + stdout: typeof mockStdout; + stderr: typeof mockStderr; + }; + + beforeEach(() => { + // Create mock stdin with write and end methods + mockStdin = new Writable() as Writable & { end: ReturnType }; + mockStdin.write = vi.fn().mockReturnValue(true); + mockStdin.end = vi.fn(); + + // Create mock stdout and stderr + mockStdout = new EventEmitter(); + mockStderr = new EventEmitter(); + + // Create mock process + mockProcess = new EventEmitter() as typeof mockProcess; + mockProcess.stdin = mockStdin; + mockProcess.stdout = mockStdout; + mockProcess.stderr = mockStderr; + + // Mock spawn to return our mock process + vi.mocked(childProcess.spawn).mockReturnValue(mockProcess as any); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should run a function successfully and return result', async () => { + const fixture: FixtureData = { + export: 'cart-validations-generate-run', + input: { + cart: { + lines: [{ quantity: 1 }] + } + }, + expectedOutput: { + operations: [] + }, + target: 'cart.validations.generate.run' + }; + + const functionRunnerPath = '/path/to/function-runner'; + const wasmPath = '/path/to/function.wasm'; + const inputQueryPath = '/path/to/query.graphql'; + const schemaPath = '/path/to/schema.graphql'; + + const resultPromise = runFunction( + functionRunnerPath, + wasmPath, + fixture, + inputQueryPath, + schemaPath + ); + + // Simulate successful function execution + setImmediate(() => { + mockStdout.emit('data', Buffer.from(JSON.stringify({ + output: { + operations: [] + } + }))); + mockProcess.emit('close', 0); + }); - const result = await runFunction(fixture, 'test-app/extensions/cart-validation-js'); + const result = await resultPromise; - expect(result).toBeDefined(); - expect(result).toHaveProperty('result'); - expect(result).toHaveProperty('error'); + expect(result).toBeDefined(); + expect(result.error).toBeNull(); + expect(result.result).toEqual({ + output: { + operations: [] + } }); - it('should handle function execution errors gracefully', async () => { - const fixture: FixtureData = { - export: 'invalid_export', - input: { cart: { lines: [] } }, - expectedOutput: {}, - target: 'cart.validations.generate.run' - }; + // Verify spawn was called with correct arguments + expect(childProcess.spawn).toHaveBeenCalledWith( + functionRunnerPath, + [ + '-f', wasmPath, + '--export', fixture.export, + '--query-path', inputQueryPath, + '--schema-path', schemaPath, + '--json', + ], + { stdio: ['pipe', 'pipe', 'pipe'] } + ); - const result = await runFunction(fixture, 'test-app/extensions/cart-validation-js'); + // Verify input was written to stdin + expect(mockStdin.write).toHaveBeenCalledWith( + JSON.stringify(fixture.input) + ); + expect(mockStdin.end).toHaveBeenCalled(); + }); - expect(result).toBeDefined(); - expect(result).toHaveProperty('result'); - expect(result).toHaveProperty('error'); + it('should handle function execution errors with non-zero exit code', async () => { + const fixture: FixtureData = { + export: 'invalid_export', + input: { cart: { lines: [] } }, + expectedOutput: {}, + target: 'cart.validations.generate.run' + }; + + const functionRunnerPath = '/path/to/function-runner'; + const wasmPath = '/path/to/function.wasm'; + const inputQueryPath = '/path/to/query.graphql'; + const schemaPath = '/path/to/schema.graphql'; + + const resultPromise = runFunction( + functionRunnerPath, + wasmPath, + fixture, + inputQueryPath, + schemaPath + ); + + // Simulate function-runner error + setImmediate(() => { + mockStderr.emit('data', Buffer.from('Error: Export not found')); + mockProcess.emit('close', 1); }); - it('should work with fixture data', async () => { - const fixture = await loadFixture('test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json'); + const result = await resultPromise; + + expect(result).toBeDefined(); + expect(result.error).toContain('function-runner failed with exit code 1'); + expect(result.error).toContain('Error: Export not found'); + expect(result.result).toBeNull(); + }); + + it('should handle process spawn errors', async () => { + const fixture: FixtureData = { + export: 'cart-validations-generate-run', + input: { cart: { lines: [] } }, + expectedOutput: {}, + target: 'cart.validations.generate.run' + }; + + const functionRunnerPath = '/path/to/nonexistent-runner'; + const wasmPath = '/path/to/function.wasm'; + const inputQueryPath = '/path/to/query.graphql'; + const schemaPath = '/path/to/schema.graphql'; + + const resultPromise = runFunction( + functionRunnerPath, + wasmPath, + fixture, + inputQueryPath, + schemaPath + ); + + // Simulate spawn error + setImmediate(() => { + const error = new Error('ENOENT: no such file or directory'); + mockProcess.emit('error', error); + }); + + const result = await resultPromise; + + expect(result).toBeDefined(); + expect(result.error).toContain('Failed to start function-runner'); + expect(result.error).toContain('ENOENT'); + expect(result.result).toBeNull(); + }); + + it('should handle invalid JSON output from function-runner', async () => { + const fixture: FixtureData = { + export: 'cart-validations-generate-run', + input: { cart: { lines: [] } }, + expectedOutput: {}, + target: 'cart.validations.generate.run' + }; + + const functionRunnerPath = '/path/to/function-runner'; + const wasmPath = '/path/to/function.wasm'; + const inputQueryPath = '/path/to/query.graphql'; + const schemaPath = '/path/to/schema.graphql'; + + const resultPromise = runFunction( + functionRunnerPath, + wasmPath, + fixture, + inputQueryPath, + schemaPath + ); + + // Simulate invalid JSON output + setImmediate(() => { + mockStdout.emit('data', Buffer.from('invalid json {{{')); + mockProcess.emit('close', 0); + }); + + const result = await resultPromise; + + expect(result).toBeDefined(); + expect(result.error).toContain('Failed to parse function-runner output'); + expect(result.result).toBeNull(); + }); + + it('should handle multiple stdout/stderr chunks', async () => { + const fixture: FixtureData = { + export: 'cart-validations-generate-run', + input: { cart: { lines: [] } }, + expectedOutput: {}, + target: 'cart.validations.generate.run' + }; + + const functionRunnerPath = '/path/to/function-runner'; + const wasmPath = '/path/to/function.wasm'; + const inputQueryPath = '/path/to/query.graphql'; + const schemaPath = '/path/to/schema.graphql'; + + const resultPromise = runFunction( + functionRunnerPath, + wasmPath, + fixture, + inputQueryPath, + schemaPath + ); + + // Simulate output in multiple chunks + const outputPart1 = '{"output":'; + const outputPart2 = '{"operations":[]}'; + const outputPart3 = '}'; + + setImmediate(() => { + mockStdout.emit('data', Buffer.from(outputPart1)); + mockStdout.emit('data', Buffer.from(outputPart2)); + mockStdout.emit('data', Buffer.from(outputPart3)); + mockProcess.emit('close', 0); + }); + + const result = await resultPromise; + + expect(result).toBeDefined(); + expect(result.error).toBeNull(); + expect(result.result).toEqual({ + output: { + operations: [] + } + }); + }); + + it('should handle output without explicit output wrapper', async () => { + const fixture: FixtureData = { + export: 'cart-validations-generate-run', + input: { cart: { lines: [] } }, + expectedOutput: {}, + target: 'cart.validations.generate.run' + }; + + const functionRunnerPath = '/path/to/function-runner'; + const wasmPath = '/path/to/function.wasm'; + const inputQueryPath = '/path/to/query.graphql'; + const schemaPath = '/path/to/schema.graphql'; + + const resultPromise = runFunction( + functionRunnerPath, + wasmPath, + fixture, + inputQueryPath, + schemaPath + ); + + // Simulate output without "output" wrapper + setImmediate(() => { + mockStdout.emit('data', Buffer.from(JSON.stringify({ + operations: [] + }))); + mockProcess.emit('close', 0); + }); - const result = await runFunction(fixture, 'test-app/extensions/cart-validation-js'); + const result = await resultPromise; - expect(result).toBeDefined(); - expect(result).toHaveProperty('result'); - expect(result).toHaveProperty('error'); + expect(result).toBeDefined(); + expect(result.error).toBeNull(); + expect(result.result).toEqual({ + output: { + operations: [] + } }); - }); \ No newline at end of file + }); +}); From 711d8639417ed62af2fbc8897024da1c4446733a Mon Sep 17 00:00:00 2001 From: Sagarika Dasgupta Date: Fri, 24 Oct 2025 10:09:20 -0500 Subject: [PATCH 3/8] target based query selection and adding a second target and fixture for the discount test function --- .../cart-validation-js/tests/default.test.js | 28 +++++------ .../discount-function/tests/default.test.js | 18 +++---- ...ure.json => cart-lines-valid-fixture.json} | 15 ++---- .../fixtures/delivery-valid-fixture.json | 50 +++++++++++++++++++ 4 files changed, 76 insertions(+), 35 deletions(-) rename test-app/extensions/discount-function/tests/fixtures/{discount-function-valid-fixture.json => cart-lines-valid-fixture.json} (82%) create mode 100644 test-app/extensions/discount-function/tests/fixtures/delivery-valid-fixture.json diff --git a/test-app/extensions/cart-validation-js/tests/default.test.js b/test-app/extensions/cart-validation-js/tests/default.test.js index c20922e..c702ff5 100644 --- a/test-app/extensions/cart-validation-js/tests/default.test.js +++ b/test-app/extensions/cart-validation-js/tests/default.test.js @@ -8,7 +8,7 @@ describe("Default Integration Test", () => { let inputQueryAST; let functionDir; let schemaPath; - let inputQueryPath; + let targeting; let functionRunnerPath; let wasmPath; @@ -38,14 +38,13 @@ describe("Default Integration Test", () => { } const functionInfo = JSON.parse(functionInfoJson); - schemaPath = functionInfo.functionSchemaPath; - inputQueryPath = functionInfo.functionInputQueryPath; + schemaPath = functionInfo.schemaPath; functionRunnerPath = functionInfo.functionRunnerPath; - wasmPath = functionInfo.functionWasmPath; + wasmPath = functionInfo.wasmPath; + targeting = functionInfo.targeting; schema = await loadSchema(schemaPath); - inputQueryAST = await loadInputQuery(inputQueryPath); - }, 20000); // 20 second timeout for building the function + }, 20000); // 20 second timeout for building and obtaining information about the function const fixturesDir = path.join(__dirname, "fixtures"); const fixtureFiles = fs @@ -56,6 +55,9 @@ describe("Default Integration Test", () => { fixtureFiles.forEach((fixtureFile) => { test(`runs ${path.relative(fixturesDir, fixtureFile)}`, async () => { const fixture = await loadFixture(fixtureFile); + const inputQueryPath = targeting[fixture.target]["inputQueryPath"]; + console.debug('inputQueryPath for fixture targeting %s is %s', fixture.target, inputQueryPath); + inputQueryAST = await loadInputQuery(inputQueryPath); // Validate fixture using our comprehensive validation system const validationResult = await validateTestAssets({ @@ -63,19 +65,17 @@ describe("Default Integration Test", () => { fixture, inputQueryAST }); - - // Assert that all validation steps pass expect(validationResult.inputQuery.errors).toHaveLength(0); expect(validationResult.inputFixture.errors).toHaveLength(0); expect(validationResult.outputFixture.errors).toHaveLength(0); // Run the actual function const runResult = await runFunction( - functionRunnerPath, - wasmPath, - fixture, - inputQueryPath, - schemaPath + functionRunnerPath, + wasmPath, + fixture, + inputQueryPath, + schemaPath ); const { result, error } = runResult; @@ -83,4 +83,4 @@ describe("Default Integration Test", () => { expect(result.output).toEqual(fixture.expectedOutput); }, 10000); }); -}); \ No newline at end of file +}); diff --git a/test-app/extensions/discount-function/tests/default.test.js b/test-app/extensions/discount-function/tests/default.test.js index f8d5577..c702ff5 100644 --- a/test-app/extensions/discount-function/tests/default.test.js +++ b/test-app/extensions/discount-function/tests/default.test.js @@ -8,7 +8,7 @@ describe("Default Integration Test", () => { let inputQueryAST; let functionDir; let schemaPath; - let inputQueryPath; + let targeting; let functionRunnerPath; let wasmPath; @@ -38,14 +38,13 @@ describe("Default Integration Test", () => { } const functionInfo = JSON.parse(functionInfoJson); - schemaPath = functionInfo.functionSchemaPath; - inputQueryPath = functionInfo.functionInputQueryPath; + schemaPath = functionInfo.schemaPath; functionRunnerPath = functionInfo.functionRunnerPath; - wasmPath = functionInfo.functionWasmPath; + wasmPath = functionInfo.wasmPath; + targeting = functionInfo.targeting; schema = await loadSchema(schemaPath); - inputQueryAST = await loadInputQuery(inputQueryPath); - }, 20000); // 20 second timeout for building the function + }, 20000); // 20 second timeout for building and obtaining information about the function const fixturesDir = path.join(__dirname, "fixtures"); const fixtureFiles = fs @@ -56,6 +55,9 @@ describe("Default Integration Test", () => { fixtureFiles.forEach((fixtureFile) => { test(`runs ${path.relative(fixturesDir, fixtureFile)}`, async () => { const fixture = await loadFixture(fixtureFile); + const inputQueryPath = targeting[fixture.target]["inputQueryPath"]; + console.debug('inputQueryPath for fixture targeting %s is %s', fixture.target, inputQueryPath); + inputQueryAST = await loadInputQuery(inputQueryPath); // Validate fixture using our comprehensive validation system const validationResult = await validateTestAssets({ @@ -63,8 +65,6 @@ describe("Default Integration Test", () => { fixture, inputQueryAST }); - - // Assert that all validation steps pass expect(validationResult.inputQuery.errors).toHaveLength(0); expect(validationResult.inputFixture.errors).toHaveLength(0); expect(validationResult.outputFixture.errors).toHaveLength(0); @@ -83,4 +83,4 @@ describe("Default Integration Test", () => { expect(result.output).toEqual(fixture.expectedOutput); }, 10000); }); -}); \ No newline at end of file +}); diff --git a/test-app/extensions/discount-function/tests/fixtures/discount-function-valid-fixture.json b/test-app/extensions/discount-function/tests/fixtures/cart-lines-valid-fixture.json similarity index 82% rename from test-app/extensions/discount-function/tests/fixtures/discount-function-valid-fixture.json rename to test-app/extensions/discount-function/tests/fixtures/cart-lines-valid-fixture.json index a932551..1dd0d5d 100644 --- a/test-app/extensions/discount-function/tests/fixtures/discount-function-valid-fixture.json +++ b/test-app/extensions/discount-function/tests/fixtures/cart-lines-valid-fixture.json @@ -1,8 +1,7 @@ { - "shopId": 55479926983, - "apiClientId": 282965573633, "payload": { "export": "cart_lines_discounts_generate_run", + "target": "cart.lines.discounts.generate.run", "input": { "cart": { "lines": [ @@ -76,14 +75,6 @@ "outputBytes": 399, "logs": [], "functionId": "0199721a-ba07-70fd-9b6f-63e9c35a8d19", - "fuelConsumed": 50920, - "target": "cart.lines.discounts.generate.run" - }, - "logType": "function_run", - "status": "success", - "source": "discount-function", - "sourceNamespace": "extensions", - "logTimestamp": "2025-09-22T15:57:24.476234Z", - "localTime": "2025-09-22 11:57:24", - "storeName": "lopert-scripts.myshopify.com" + "fuelConsumed": 50920 + } } diff --git a/test-app/extensions/discount-function/tests/fixtures/delivery-valid-fixture.json b/test-app/extensions/discount-function/tests/fixtures/delivery-valid-fixture.json new file mode 100644 index 0000000..2b2b0a9 --- /dev/null +++ b/test-app/extensions/discount-function/tests/fixtures/delivery-valid-fixture.json @@ -0,0 +1,50 @@ +{ + "payload": { + "export": "cart_delivery_options_discounts_generate_run", + "target": "cart.delivery-options.discounts.generate.run", + "input": { + "discount": { + "discountClasses": ["SHIPPING"] + }, + "cart": { + "deliveryGroups": [ + { + "id": "gid://shopify/CartDeliveryGroup/1" + } + ], + "cost": { + "subtotalAmount": { + "amount": "100.0" + } + } + } + }, + "output": { + "operations": [ + { + "deliveryDiscountsAdd": { + "candidates": [ + { + "associatedDiscountCode": null, + "message": "FREE DELIVERY", + "targets": [ + { + "deliveryGroup": { + "id": "gid://shopify/CartDeliveryGroup/1" + } + } + ], + "value": { + "percentage": { + "value": "100.0" + } + } + } + ], + "selectionStrategy": "ALL" + } + } + ] + } + } +} \ No newline at end of file From e24dd93d3f6ee41f844c2c395f35c6b3aeb422e0 Mon Sep 17 00:00:00 2001 From: Sagarika Dasgupta Date: Tue, 28 Oct 2025 08:12:38 -0500 Subject: [PATCH 4/8] moving function info to a helper method --- src/methods/get-function-info.ts | 47 +++++++++++++++++++ src/wasm-testing-helpers.ts | 2 + .../cart-validation-js/tests/default.test.js | 28 ++--------- .../discount-function/tests/default.test.js | 28 ++--------- 4 files changed, 55 insertions(+), 50 deletions(-) create mode 100644 src/methods/get-function-info.ts diff --git a/src/methods/get-function-info.ts b/src/methods/get-function-info.ts new file mode 100644 index 0000000..9c70b93 --- /dev/null +++ b/src/methods/get-function-info.ts @@ -0,0 +1,47 @@ +/** + * Retrieve function information from the Shopify CLI + */ + +import { execSync } from 'child_process'; + +/** + * Information about a Shopify function + */ +export interface FunctionInfo { + schemaPath: string; + functionRunnerPath: string; + wasmPath: string; + targeting: Record; +} + +/** + * Retrieves function information from the Shopify CLI + * @param {string} functionDir - The directory path of the function + * @returns {FunctionInfo} Function information including schemaPath, functionRunnerPath, wasmPath, and targeting + * @throws {Error} If the CLI command is not available or fails + */ +export function getFunctionInfo(functionDir: string): FunctionInfo { + let functionInfoJson: string; + + try { + functionInfoJson = execSync( + `shopify app function info --json --path ${functionDir}`, + { + encoding: 'utf-8' + } + ); + } catch (error) { + // Check if the error is due to the command not being found + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes('Command app function info not found')) { + throw new Error( + 'The "shopify app function info" command is not available in your CLI version.\n' + + 'Please upgrade to the latest version:\n' + + ' npm install -g @shopify/cli@latest\n\n' + ); + } + throw error; + } + + return JSON.parse(functionInfoJson) as FunctionInfo; +} diff --git a/src/wasm-testing-helpers.ts b/src/wasm-testing-helpers.ts index c3ed581..2f02140 100644 --- a/src/wasm-testing-helpers.ts +++ b/src/wasm-testing-helpers.ts @@ -11,6 +11,7 @@ export { loadSchema } from "./methods/load-schema.js"; export { loadInputQuery } from "./methods/load-input-query.js"; export { buildFunction } from "./methods/build-function.js"; export { runFunction } from "./methods/run-function.js"; +export { getFunctionInfo } from "./methods/get-function-info.js"; export { validateTestAssets } from "./methods/validate-test-assets.js"; export { validateInputQuery } from "./methods/validate-input-query.js"; export { validateFixtureOutput } from "./methods/validate-fixture-output.js"; @@ -20,6 +21,7 @@ export { validateFixtureInput } from "./methods/validate-fixture-input.js"; export type { FixtureData } from "./methods/load-fixture.js"; export type { BuildFunctionResult } from "./methods/build-function.js"; export type { RunFunctionResult } from "./methods/run-function.js"; +export type { FunctionInfo } from "./methods/get-function-info.js"; export type { ValidateTestAssetsOptions, CompleteValidationResult, diff --git a/test-app/extensions/cart-validation-js/tests/default.test.js b/test-app/extensions/cart-validation-js/tests/default.test.js index c702ff5..ba2e9db 100644 --- a/test-app/extensions/cart-validation-js/tests/default.test.js +++ b/test-app/extensions/cart-validation-js/tests/default.test.js @@ -1,7 +1,6 @@ import path from "path"; import fs from "fs"; -import { execSync } from "child_process"; -import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery } from "@shopify/shopify-function-test-helpers"; +import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery, getFunctionInfo } from "@shopify/shopify-function-test-helpers"; describe("Default Integration Test", () => { let schema; @@ -17,27 +16,7 @@ describe("Default Integration Test", () => { await buildFunction(functionDir); // Get function info from Shopify CLI - let functionInfoJson; - try { - functionInfoJson = execSync( - `shopify app function info --json --path ${functionDir}`, - { - encoding: 'utf-8' - } - ); - } catch (error) { - // Check if the error is due to the command not being found - if (error.message.includes('Command app function info not found')) { - throw new Error( - 'The "shopify app function info" command is not available in your CLI version.\n' + - 'Please upgrade to the latest version:\n' + - ' npm install -g @shopify/cli@latest\n\n' - ); - } - throw error; - } - - const functionInfo = JSON.parse(functionInfoJson); + const functionInfo = getFunctionInfo(functionDir); schemaPath = functionInfo.schemaPath; functionRunnerPath = functionInfo.functionRunnerPath; wasmPath = functionInfo.wasmPath; @@ -55,8 +34,7 @@ describe("Default Integration Test", () => { fixtureFiles.forEach((fixtureFile) => { test(`runs ${path.relative(fixturesDir, fixtureFile)}`, async () => { const fixture = await loadFixture(fixtureFile); - const inputQueryPath = targeting[fixture.target]["inputQueryPath"]; - console.debug('inputQueryPath for fixture targeting %s is %s', fixture.target, inputQueryPath); + const inputQueryPath = targeting[fixture.target].inputQueryPath; inputQueryAST = await loadInputQuery(inputQueryPath); // Validate fixture using our comprehensive validation system diff --git a/test-app/extensions/discount-function/tests/default.test.js b/test-app/extensions/discount-function/tests/default.test.js index c702ff5..ba2e9db 100644 --- a/test-app/extensions/discount-function/tests/default.test.js +++ b/test-app/extensions/discount-function/tests/default.test.js @@ -1,7 +1,6 @@ import path from "path"; import fs from "fs"; -import { execSync } from "child_process"; -import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery } from "@shopify/shopify-function-test-helpers"; +import { buildFunction, loadFixture, runFunction, validateTestAssets, loadSchema, loadInputQuery, getFunctionInfo } from "@shopify/shopify-function-test-helpers"; describe("Default Integration Test", () => { let schema; @@ -17,27 +16,7 @@ describe("Default Integration Test", () => { await buildFunction(functionDir); // Get function info from Shopify CLI - let functionInfoJson; - try { - functionInfoJson = execSync( - `shopify app function info --json --path ${functionDir}`, - { - encoding: 'utf-8' - } - ); - } catch (error) { - // Check if the error is due to the command not being found - if (error.message.includes('Command app function info not found')) { - throw new Error( - 'The "shopify app function info" command is not available in your CLI version.\n' + - 'Please upgrade to the latest version:\n' + - ' npm install -g @shopify/cli@latest\n\n' - ); - } - throw error; - } - - const functionInfo = JSON.parse(functionInfoJson); + const functionInfo = getFunctionInfo(functionDir); schemaPath = functionInfo.schemaPath; functionRunnerPath = functionInfo.functionRunnerPath; wasmPath = functionInfo.wasmPath; @@ -55,8 +34,7 @@ describe("Default Integration Test", () => { fixtureFiles.forEach((fixtureFile) => { test(`runs ${path.relative(fixturesDir, fixtureFile)}`, async () => { const fixture = await loadFixture(fixtureFile); - const inputQueryPath = targeting[fixture.target]["inputQueryPath"]; - console.debug('inputQueryPath for fixture targeting %s is %s', fixture.target, inputQueryPath); + const inputQueryPath = targeting[fixture.target].inputQueryPath; inputQueryAST = await loadInputQuery(inputQueryPath); // Validate fixture using our comprehensive validation system From 9a7a806f33a33e658c11c732d1cfffcc7b2cc377 Mon Sep 17 00:00:00 2001 From: Sagarika Dasgupta Date: Wed, 29 Oct 2025 10:19:56 -0500 Subject: [PATCH 5/8] making it spawn instead of execsync because thats what we have used in other commands --- src/methods/get-function-info.ts | 81 +++++++++++++------ .../cart-validation-js/tests/default.test.js | 2 +- .../discount-function/tests/default.test.js | 2 +- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/methods/get-function-info.ts b/src/methods/get-function-info.ts index 9c70b93..2d15be7 100644 --- a/src/methods/get-function-info.ts +++ b/src/methods/get-function-info.ts @@ -2,7 +2,8 @@ * Retrieve function information from the Shopify CLI */ -import { execSync } from 'child_process'; +import { spawn } from 'child_process'; +import path from 'path'; /** * Information about a Shopify function @@ -17,31 +18,61 @@ export interface FunctionInfo { /** * Retrieves function information from the Shopify CLI * @param {string} functionDir - The directory path of the function - * @returns {FunctionInfo} Function information including schemaPath, functionRunnerPath, wasmPath, and targeting + * @returns {Promise} Function information including schemaPath, functionRunnerPath, wasmPath, and targeting * @throws {Error} If the CLI command is not available or fails */ -export function getFunctionInfo(functionDir: string): FunctionInfo { - let functionInfoJson: string; - - try { - functionInfoJson = execSync( - `shopify app function info --json --path ${functionDir}`, - { - encoding: 'utf-8' +export async function getFunctionInfo(functionDir: string): Promise { + const resolvedFunctionDir = path.resolve(functionDir); + const appRootDir = path.dirname(resolvedFunctionDir); + const functionName = path.basename(resolvedFunctionDir); + + return new Promise((resolve, reject) => { + const shopifyProcess = spawn('shopify', [ + 'app', 'function', 'info', + '--json', + '--path', functionName + ], { + cwd: appRootDir, + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + + shopifyProcess.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + shopifyProcess.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + shopifyProcess.on('close', (code) => { + if (code !== 0) { + // Check if the error is due to the command not being found + if (stderr.includes('Command app function info not found') || stderr.includes('command not found')) { + reject(new Error( + 'The "shopify app function info" command is not available in your CLI version.\n' + + 'Please upgrade to the latest version:\n' + + ' npm install -g @shopify/cli@latest\n\n' + )); + return; + } + reject(new Error(`Function info command failed with exit code ${code}: ${stderr}`)); + return; + } + + try { + const functionInfo = JSON.parse(stdout.trim()) as FunctionInfo; + resolve(functionInfo); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + reject(new Error(`Failed to parse function info JSON: ${errorMessage}\nOutput: ${stdout}`)); } - ); - } catch (error) { - // Check if the error is due to the command not being found - const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes('Command app function info not found')) { - throw new Error( - 'The "shopify app function info" command is not available in your CLI version.\n' + - 'Please upgrade to the latest version:\n' + - ' npm install -g @shopify/cli@latest\n\n' - ); - } - throw error; - } - - return JSON.parse(functionInfoJson) as FunctionInfo; + }); + + shopifyProcess.on('error', (error) => { + reject(new Error(`Failed to start shopify function info command: ${error.message}`)); + }); + }); } diff --git a/test-app/extensions/cart-validation-js/tests/default.test.js b/test-app/extensions/cart-validation-js/tests/default.test.js index ba2e9db..ab24dd7 100644 --- a/test-app/extensions/cart-validation-js/tests/default.test.js +++ b/test-app/extensions/cart-validation-js/tests/default.test.js @@ -16,7 +16,7 @@ describe("Default Integration Test", () => { await buildFunction(functionDir); // Get function info from Shopify CLI - const functionInfo = getFunctionInfo(functionDir); + const functionInfo = await getFunctionInfo(functionDir); schemaPath = functionInfo.schemaPath; functionRunnerPath = functionInfo.functionRunnerPath; wasmPath = functionInfo.wasmPath; diff --git a/test-app/extensions/discount-function/tests/default.test.js b/test-app/extensions/discount-function/tests/default.test.js index ba2e9db..ab24dd7 100644 --- a/test-app/extensions/discount-function/tests/default.test.js +++ b/test-app/extensions/discount-function/tests/default.test.js @@ -16,7 +16,7 @@ describe("Default Integration Test", () => { await buildFunction(functionDir); // Get function info from Shopify CLI - const functionInfo = getFunctionInfo(functionDir); + const functionInfo = await getFunctionInfo(functionDir); schemaPath = functionInfo.schemaPath; functionRunnerPath = functionInfo.functionRunnerPath; wasmPath = functionInfo.wasmPath; From bfbf934e5a99fd273c443e2dbf1e6c66df4bc9c8 Mon Sep 17 00:00:00 2001 From: Sagarika Dasgupta Date: Wed, 29 Oct 2025 13:11:07 -0500 Subject: [PATCH 6/8] reformating run-function args and function runner output handling --- src/methods/run-function.ts | 13 ++++++++--- .../cart-validation-js/tests/default.test.js | 2 +- .../discount-function/tests/default.test.js | 2 +- test/methods/run-function.test.ts | 23 ++++++++----------- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/methods/run-function.ts b/src/methods/run-function.ts index c2276be..8772c5f 100644 --- a/src/methods/run-function.ts +++ b/src/methods/run-function.ts @@ -27,9 +27,9 @@ export interface RunFunctionResult { */ export async function runFunction( + fixture: FixtureData, functionRunnerPath: string, wasmPath: string, - fixture: FixtureData, queryPath: string, schemaPath: string, ): Promise { @@ -72,8 +72,16 @@ export async function runFunction( const result = JSON.parse(stdout); // function-runner output format: { output: {...} } + if (!result.output) { + resolve({ + result: null, + error: `function-runner returned unexpected format - missing 'output' field. Received: ${JSON.stringify(result)}` + }); + return; + } + resolve({ - result: { output: result.output || result }, + result: { output: result.output }, error: null }); } catch (parseError) { @@ -85,7 +93,6 @@ export async function runFunction( }); runnerProcess.on('error', (error) => { - const endTime = Date.now(); resolve({ result: null, error: `Failed to start function-runner: ${error.message}` diff --git a/test-app/extensions/cart-validation-js/tests/default.test.js b/test-app/extensions/cart-validation-js/tests/default.test.js index ab24dd7..91d95b1 100644 --- a/test-app/extensions/cart-validation-js/tests/default.test.js +++ b/test-app/extensions/cart-validation-js/tests/default.test.js @@ -49,9 +49,9 @@ describe("Default Integration Test", () => { // Run the actual function const runResult = await runFunction( + fixture, functionRunnerPath, wasmPath, - fixture, inputQueryPath, schemaPath ); diff --git a/test-app/extensions/discount-function/tests/default.test.js b/test-app/extensions/discount-function/tests/default.test.js index ab24dd7..91d95b1 100644 --- a/test-app/extensions/discount-function/tests/default.test.js +++ b/test-app/extensions/discount-function/tests/default.test.js @@ -49,9 +49,9 @@ describe("Default Integration Test", () => { // Run the actual function const runResult = await runFunction( + fixture, functionRunnerPath, wasmPath, - fixture, inputQueryPath, schemaPath ); diff --git a/test/methods/run-function.test.ts b/test/methods/run-function.test.ts index 94149c4..6c2058f 100644 --- a/test/methods/run-function.test.ts +++ b/test/methods/run-function.test.ts @@ -62,9 +62,9 @@ describe('runFunction', () => { const schemaPath = '/path/to/schema.graphql'; const resultPromise = runFunction( + fixture, functionRunnerPath, wasmPath, - fixture, inputQueryPath, schemaPath ); @@ -123,9 +123,9 @@ describe('runFunction', () => { const schemaPath = '/path/to/schema.graphql'; const resultPromise = runFunction( + fixture, functionRunnerPath, wasmPath, - fixture, inputQueryPath, schemaPath ); @@ -158,9 +158,9 @@ describe('runFunction', () => { const schemaPath = '/path/to/schema.graphql'; const resultPromise = runFunction( + fixture, functionRunnerPath, wasmPath, - fixture, inputQueryPath, schemaPath ); @@ -193,9 +193,9 @@ describe('runFunction', () => { const schemaPath = '/path/to/schema.graphql'; const resultPromise = runFunction( + fixture, functionRunnerPath, wasmPath, - fixture, inputQueryPath, schemaPath ); @@ -227,9 +227,9 @@ describe('runFunction', () => { const schemaPath = '/path/to/schema.graphql'; const resultPromise = runFunction( + fixture, functionRunnerPath, wasmPath, - fixture, inputQueryPath, schemaPath ); @@ -257,7 +257,7 @@ describe('runFunction', () => { }); }); - it('should handle output without explicit output wrapper', async () => { + it('should reject output without explicit output wrapper', async () => { const fixture: FixtureData = { export: 'cart-validations-generate-run', input: { cart: { lines: [] } }, @@ -271,9 +271,9 @@ describe('runFunction', () => { const schemaPath = '/path/to/schema.graphql'; const resultPromise = runFunction( + fixture, functionRunnerPath, wasmPath, - fixture, inputQueryPath, schemaPath ); @@ -289,11 +289,8 @@ describe('runFunction', () => { const result = await resultPromise; expect(result).toBeDefined(); - expect(result.error).toBeNull(); - expect(result.result).toEqual({ - output: { - operations: [] - } - }); + expect(result.error).toContain('function-runner returned unexpected format'); + expect(result.error).toContain('missing \'output\' field'); + expect(result.result).toBeNull(); }); }); From 95b7712153bac93349065c94a7c29a2e6cf61779 Mon Sep 17 00:00:00 2001 From: Sagarika Dasgupta Date: Fri, 31 Oct 2025 10:52:42 -0500 Subject: [PATCH 7/8] tests for get-function-info method --- test/methods/get-function-info.test.ts | 150 +++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 test/methods/get-function-info.test.ts diff --git a/test/methods/get-function-info.test.ts b/test/methods/get-function-info.test.ts new file mode 100644 index 0000000..0b862f6 --- /dev/null +++ b/test/methods/get-function-info.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { EventEmitter } from "events"; + +import { getFunctionInfo } from "../../src/methods/get-function-info.ts"; + +// Mock child_process module +vi.mock("child_process", () => ({ + spawn: vi.fn(), +})); + +describe("getFunctionInfo", () => { + let mockSpawn: any; + let mockProcess: any; + + beforeEach(async () => { + // Import the mocked spawn function + const { spawn } = await import("child_process"); + mockSpawn = spawn as any; + + // Create a mock process object that extends EventEmitter + mockProcess = new EventEmitter(); + mockProcess.stdout = new EventEmitter(); + mockProcess.stderr = new EventEmitter(); + + // Reset the mock before each test + mockSpawn.mockReturnValue(mockProcess); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should successfully retrieve function info from Shopify CLI", async () => { + const mockFunctionInfo = { + schemaPath: "/path/to/schema.graphql", + functionRunnerPath: "/path/to/function-runner.wasm", + wasmPath: "/path/to/function.wasm", + targeting: { + target: "purchase.payment-customization.run", + version: "2024-01", + }, + }; + + const promise = getFunctionInfo("/path/to/extensions/my-function"); + + // Simulate successful CLI response + setTimeout(() => { + mockProcess.stdout.emit("data", JSON.stringify(mockFunctionInfo)); + mockProcess.emit("close", 0); + }, 10); + + const result = await promise; + + expect(result).toEqual(mockFunctionInfo); + expect(mockSpawn).toHaveBeenCalledWith( + "shopify", + ["app", "function", "info", "--json", "--path", "my-function"], + { + cwd: "/path/to/extensions", + stdio: ["pipe", "pipe", "pipe"], + }, + ); + }); + + it("should reject when CLI command is not found", async () => { + const promise = getFunctionInfo("/path/to/extensions/my-function"); + + setTimeout(() => { + mockProcess.stderr.emit( + "data", + "Error: Command app function info not found", + ); + mockProcess.emit("close", 1); + }, 10); + + await expect(promise).rejects.toThrow( + 'The "shopify app function info" command is not available', + ); + await expect(promise).rejects.toThrow( + "Please upgrade to the latest version", + ); + }); + + it("should reject when shopify command is not found", async () => { + const promise = getFunctionInfo("/path/to/extensions/my-function"); + + setTimeout(() => { + mockProcess.stderr.emit("data", "shopify: command not found"); + mockProcess.emit("close", 127); + }, 10); + + await expect(promise).rejects.toThrow( + 'The "shopify app function info" command is not available', + ); + }); + + it("should reject when CLI command fails with non-zero exit code", async () => { + const promise = getFunctionInfo("/path/to/extensions/my-function"); + + setTimeout(() => { + mockProcess.stderr.emit("data", "Error: Function not found\n"); + mockProcess.emit("close", 1); + }, 10); + + await expect(promise).rejects.toThrow( + "Function info command failed with exit code 1", + ); + await expect(promise).rejects.toThrow("Error: Function not found"); + }); + + it("should reject when JSON parsing fails", async () => { + const promise = getFunctionInfo("/path/to/extensions/my-function"); + + setTimeout(() => { + mockProcess.stdout.emit("data", "Invalid JSON output"); + mockProcess.emit("close", 0); + }, 10); + + await expect(promise).rejects.toThrow("Failed to parse function info JSON"); + await expect(promise).rejects.toThrow("Invalid JSON output"); + }); + + it("should reject when spawn process emits an error", async () => { + const promise = getFunctionInfo("/path/to/extensions/my-function"); + + setTimeout(() => { + mockProcess.emit("error", new Error("ENOENT: spawn failed")); + }, 10); + + await expect(promise).rejects.toThrow( + "Failed to start shopify function info command", + ); + await expect(promise).rejects.toThrow("ENOENT: spawn failed"); + }); + + it("should accumulate stderr output for error messages", async () => { + const promise = getFunctionInfo("/path/to/extensions/my-function"); + + setTimeout(() => { + mockProcess.stderr.emit("data", "Error line 1\n"); + mockProcess.stderr.emit("data", "Error line 2\n"); + mockProcess.stderr.emit("data", "Error line 3"); + mockProcess.emit("close", 1); + }, 10); + + await expect(promise).rejects.toThrow("Error line 1"); + await expect(promise).rejects.toThrow("Error line 2"); + await expect(promise).rejects.toThrow("Error line 3"); + }); +}); From 6f7ce70a849e001d89b8e52e04e7e14cd57b244e Mon Sep 17 00:00:00 2001 From: Sagarika Dasgupta Date: Fri, 31 Oct 2025 15:49:22 -0500 Subject: [PATCH 8/8] Simplifying import and mocking for spawn, deconstructing getFunctionInfo using property, extracting expected output into variable in test to address PR comments --- .../cart-validation-js/tests/default.test.js | 5 +-- .../discount-function/tests/default.test.js | 5 +-- test/methods/get-function-info.test.ts | 12 +++---- test/methods/run-function.test.ts | 31 +++++++++---------- 4 files changed, 21 insertions(+), 32 deletions(-) diff --git a/test-app/extensions/cart-validation-js/tests/default.test.js b/test-app/extensions/cart-validation-js/tests/default.test.js index 91d95b1..6fcbf25 100644 --- a/test-app/extensions/cart-validation-js/tests/default.test.js +++ b/test-app/extensions/cart-validation-js/tests/default.test.js @@ -17,10 +17,7 @@ describe("Default Integration Test", () => { // Get function info from Shopify CLI const functionInfo = await getFunctionInfo(functionDir); - schemaPath = functionInfo.schemaPath; - functionRunnerPath = functionInfo.functionRunnerPath; - wasmPath = functionInfo.wasmPath; - targeting = functionInfo.targeting; + ({ schemaPath, functionRunnerPath, wasmPath, targeting } = functionInfo); schema = await loadSchema(schemaPath); }, 20000); // 20 second timeout for building and obtaining information about the function diff --git a/test-app/extensions/discount-function/tests/default.test.js b/test-app/extensions/discount-function/tests/default.test.js index 91d95b1..6fcbf25 100644 --- a/test-app/extensions/discount-function/tests/default.test.js +++ b/test-app/extensions/discount-function/tests/default.test.js @@ -17,10 +17,7 @@ describe("Default Integration Test", () => { // Get function info from Shopify CLI const functionInfo = await getFunctionInfo(functionDir); - schemaPath = functionInfo.schemaPath; - functionRunnerPath = functionInfo.functionRunnerPath; - wasmPath = functionInfo.wasmPath; - targeting = functionInfo.targeting; + ({ schemaPath, functionRunnerPath, wasmPath, targeting } = functionInfo); schema = await loadSchema(schemaPath); }, 20000); // 20 second timeout for building and obtaining information about the function diff --git a/test/methods/get-function-info.test.ts b/test/methods/get-function-info.test.ts index 0b862f6..cb86ebf 100644 --- a/test/methods/get-function-info.test.ts +++ b/test/methods/get-function-info.test.ts @@ -1,28 +1,24 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { EventEmitter } from "events"; +import { spawn } from "child_process"; import { getFunctionInfo } from "../../src/methods/get-function-info.ts"; -// Mock child_process module vi.mock("child_process", () => ({ spawn: vi.fn(), })); describe("getFunctionInfo", () => { - let mockSpawn: any; + const mockSpawn = vi.mocked(spawn); let mockProcess: any; - beforeEach(async () => { - // Import the mocked spawn function - const { spawn } = await import("child_process"); - mockSpawn = spawn as any; - + beforeEach(() => { // Create a mock process object that extends EventEmitter mockProcess = new EventEmitter(); mockProcess.stdout = new EventEmitter(); mockProcess.stderr = new EventEmitter(); - // Reset the mock before each test + // Configure the mock to return our mock process mockSpawn.mockReturnValue(mockProcess); }); diff --git a/test/methods/run-function.test.ts b/test/methods/run-function.test.ts index 6c2058f..7c88f97 100644 --- a/test/methods/run-function.test.ts +++ b/test/methods/run-function.test.ts @@ -1,14 +1,16 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { EventEmitter } from 'events'; import { Writable } from 'stream'; -import * as childProcess from 'child_process'; +import { spawn } from 'child_process'; import { runFunction } from '../../src/methods/run-function.ts'; import { FixtureData } from '../../src/methods/load-fixture.ts'; -// Mock child_process -vi.mock('child_process'); +vi.mock('child_process', () => ({ + spawn: vi.fn(), +})); describe('runFunction', () => { + const mockSpawn = vi.mocked(spawn); let mockStdin: Writable & { end: ReturnType }; let mockStdout: EventEmitter; let mockStderr: EventEmitter; @@ -34,8 +36,8 @@ describe('runFunction', () => { mockProcess.stdout = mockStdout; mockProcess.stderr = mockStderr; - // Mock spawn to return our mock process - vi.mocked(childProcess.spawn).mockReturnValue(mockProcess as any); + // Configure the mock to return our mock process + mockSpawn.mockReturnValue(mockProcess as any); }); afterEach(() => { @@ -70,12 +72,13 @@ describe('runFunction', () => { ); // Simulate successful function execution + const expectedOutput = { + output: { + operations: [] + } + }; setImmediate(() => { - mockStdout.emit('data', Buffer.from(JSON.stringify({ - output: { - operations: [] - } - }))); + mockStdout.emit('data', Buffer.from(JSON.stringify(expectedOutput))); mockProcess.emit('close', 0); }); @@ -83,14 +86,10 @@ describe('runFunction', () => { expect(result).toBeDefined(); expect(result.error).toBeNull(); - expect(result.result).toEqual({ - output: { - operations: [] - } - }); + expect(result.result).toEqual(expectedOutput); // Verify spawn was called with correct arguments - expect(childProcess.spawn).toHaveBeenCalledWith( + expect(mockSpawn).toHaveBeenCalledWith( functionRunnerPath, [ '-f', wasmPath,