diff --git a/packages/core/globals.ts b/packages/core/globals.ts index bd0f0f6..9d2999d 100644 --- a/packages/core/globals.ts +++ b/packages/core/globals.ts @@ -27,9 +27,10 @@ export const globalTempDir = join(globalWorkspacePath, '.temp') const callbacks = [] export const onExit = (callback) => callbacks.push(callback) -const runBeforeExitCallbacks = () => { +const runBeforeExitCallbacks = (code) => { + console.log('Running before exit callbacks', code) callbacks.forEach(cb => { - if (!cb.called) cb() + if (!cb.called) cb(code) cb.called = true }) } @@ -47,9 +48,9 @@ export const initialize = (tempDir = globalTempDir) => { process.on('exit', runBeforeExitCallbacks); - process.on('SIGINT', () => { - runBeforeExitCallbacks() - process.exit(0) + process.on('SIGINT', (code) => { + runBeforeExitCallbacks(code) + process.exit(code as any) }) // Always clear the temp directory on exit diff --git a/packages/core/start.ts b/packages/core/start.ts index 32331b3..e34e499 100644 --- a/packages/core/start.ts +++ b/packages/core/start.ts @@ -20,14 +20,8 @@ export default async function ( opts: UserConfig = {} ) { const isDesktopTarget = isDesktop(target) const isMobileTarget = isMobile(target) - // if (target === 'electron' && root !== process.cwd()) { - // console.log(`\n👎 Cannot start ${chalk.bold(name)} (electron) when targeting a different root.\n`) - // process.exit(1) - // } - console.log(`\n✊ Starting ${chalk.bold(chalk.greenBright(name))} for ${target}\n`) - // Create URLs that will be shared with the frontend if (isMobileTarget) resolvedConfig.services = updateServicesWithLocalIP(resolvedConfig.services) diff --git a/packages/core/templates/electron/main.ts b/packages/core/templates/electron/main.ts index d52f05a..041598b 100644 --- a/packages/core/templates/electron/main.ts +++ b/packages/core/templates/electron/main.ts @@ -259,12 +259,8 @@ runPlugins(null, 'preload').then(() => { }) if (active) { - for (let id in active) serviceOn(id, 'status', (event) => event.returnValue = active[id].status) - - ipcMain.on('commoners:services', (event) => event.returnValue = services.sanitize(active)) - - process.env.COMMONERS_SERVICES = JSON.stringify(services.sanitize(active)) // Expose to renderer process (and ensure URLs are correct) + ipcMain.on('commoners:services', (event) => event.returnValue = services.sanitize(active)) // Expose to renderer process (and ensure URLs are correct) } // Proxy the services through the custom protocol @@ -303,5 +299,5 @@ app.on('before-quit', async (ev) => { const result = await runPlugins(null, 'unload') if (result.includes(false)) return - try { services.close() } catch (err) { console.error(err); } finally { app.exit() } + try { services.close() } catch (err) { console.error(err); } finally { app.exit() } // Exit gracefully }); \ No newline at end of file diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index a756397..f50e84e 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -12,7 +12,6 @@ import { resolve } from 'node:path' import { name } from './commoners.config' import { projectBase, registerBuildTest, registerStartTest, serviceTests } from './utils' - describe('Custom project base is loaded', () => { test('Config is resolved', () => { @@ -28,49 +27,53 @@ describe('Custom project base is loaded', () => { describe('Start', () => { - // registerStartTest('Web') + registerStartTest('Web') - registerStartTest('Desktop', { target: 'electron'}) + registerStartTest( + 'Desktop', + { target: 'electron'}, + false // NOTE: Valid test suite—but causes a SIGABRT that results in a crash + ) // NOTE: Skipped because Ruby Gems needs to be updated registerStartTest('Mobile', { target: 'mobile' }, false) }) -// describe('Share', () => { +describe('Share', () => { -// describe('Share all services', () => { -// const output = share(projectBase) -// serviceTests.share.basic(output) -// serviceTests.echo('http', output) -// serviceTests.echo('express', output) -// // serviceTests.echo('python') -// }) + describe('Share all services', () => { + const output = share(projectBase) + serviceTests.share.basic(output) + serviceTests.echo('http', output) + serviceTests.echo('express', output) + serviceTests.echo('manual', output) + }) -// describe('Share specific service', () => { + describe('Share specific service', () => { -// const service = 'http' + const service = 'http' -// const output = share(projectBase, { services: [ service ] }) + const output = share(projectBase, { services: [ service ] }) -// serviceTests.echo(service, output) + serviceTests.echo(service, output) -// // NOTE: Add a check to see if other services fail + // NOTE: Add a check to see if other services fail -// }) -// }) + }) +}) -// describe('Build and Launch', () => { -// registerBuildTest('Web', { target: 'web' }) -// registerBuildTest('PWA', { target: 'pwa' }) +describe('Build and Launch', () => { + registerBuildTest('Web', { target: 'web' }) + registerBuildTest('PWA', { target: 'pwa' }) -// registerBuildTest( -// 'Desktop', -// { target: 'electron' } -// ) + registerBuildTest( + 'Desktop', + { target: 'electron' } + ) -// // registerBuildTest('Mobile', { target: 'mobile' }, false) -// }) + // registerBuildTest('Mobile', { target: 'mobile' }, false) +}) diff --git a/packages/core/tests/utils.ts b/packages/core/tests/utils.ts index c2b8059..6036169 100644 --- a/packages/core/tests/utils.ts +++ b/packages/core/tests/utils.ts @@ -19,7 +19,7 @@ const getServices = (registrationOutput) => ((registrationOutput.commoners ?? re export const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) const e2eTests = { - basic: (registrationOutput, { target }) => { + basic: (registrationOutput, { target }, mode = 'dev') => { const normalizedTarget = normalizeTarget(target) @@ -34,16 +34,31 @@ const e2eTests = { expect(target).toBe(normalizedTarget); expect('echo' in plugins).toBe(true); expect(services).instanceOf(Object) - expect(ready).instanceOf(Object) // Really is promise. Is passed as an object + expect(ready).instanceOf(Object) // Resolved Promise - // Services cleared on mobile and web (not remote...) if (normalizedTarget === 'desktop') { + expect(commoners.quit).instanceOf(Object) + } + + const isDev = mode === 'dev' + Object.entries(config.services).forEach(([name, service]) => { - expect(name in services).toBe(true); - expect(typeof services[name].url).toBe('string'); - if ('port' in service) expect(parseInt(new URL(services[name].url).port)).toBe(service.port) + + // Web / PWA / Mobile builds will have cleared services (that are not remote) + expect(name in services).toBe(isDev); + + if (isDev) { + expect(typeof services[name].url).toBe('string'); + if ('port' in service) expect(parseInt(new URL(services[name].url).port)).toBe(service.port) + } + + if (normalizedTarget === 'desktop') { + expect(typeof services[name].filepath).toBe('string'); + expect(services[name].status).toBe(true) + expect(services[name].onActivityDetected).instanceOf(Object) // Function + expect(services[name].onClosed).instanceOf(Object) // Function + } }) - } }); }) @@ -53,6 +68,7 @@ const e2eTests = { export const registerStartTest = (name, { target = 'web' } = {}, enabled = true) => { const describeCommand = enabled ? describe : describe.skip + describeCommand(name, () => { // start(projectBase, { target }) @@ -101,7 +117,7 @@ export const registerBuildTest = (name, { target = 'web'} = {}, enabled = true) describeFn('Launched application tests', async () => { const output = startBrowserTest({ launch: opts }) - e2eTests.basic(output, { target }) + e2eTests.basic(output, { target }, 'local') }) }) @@ -145,31 +161,7 @@ export const serviceTests = { const services = getServices(registrationOutput) // Request an echo response - const serviceUrl = services[id].url.replace('localhost', '127.0.0.1') - const url = new URL('echo', serviceUrl) - console.log(id, url.href) - // const res = await fetch(url, { - // method: "POST", - // body: JSON.stringify({ randomNumber }) - // }).then(res => res.json()) - // .catch(e => { - // console.log(e) - // return {} - // }) - - const res = await registrationOutput.page.evaluate(({ id, randomNumber }) => fetch(new URL('echo', commoners.services[id].url), { - method: "POST", - body: JSON.stringify({ randomNumber }) - }).then(res => res.json()), { - id, - randomNumber - }).catch(e => { - console.log(e) - return {} - }) - - console.log('Result', id, res) - + const res = await fetch(new URL('echo', services[id].url), { method: "POST", body: JSON.stringify({ randomNumber }) }).then(res => res.json()) expect(res.randomNumber).toBe(randomNumber) }) }, diff --git a/packages/testing/index.ts b/packages/testing/index.ts index 1351fc2..de81c87 100644 --- a/packages/testing/index.ts +++ b/packages/testing/index.ts @@ -1,5 +1,5 @@ -import { afterAll, beforeAll, expect, describe, test } from 'vitest' +import { afterAll, beforeAll, expect, describe, vi, test } from 'vitest' import { build, loadConfigFromFile, @@ -129,6 +129,12 @@ export const startBrowserTest = (customProps: Partial = {}, projectB // Launched Electron Instance if (isElectron) { + // Ensure Electron will exit gracefully + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + mockExit.mockRestore() + }); + + await sleep(5 * 1000) // Wait for five seconds for Electron to open const browserURL = `http://localhost:${electronDebugPort}` @@ -140,15 +146,11 @@ export const startBrowserTest = (customProps: Partial = {}, projectB delete output.browser delete output.page + // Connect to browser WS Endpoint const browserWSEndpoint = endpoint.replace('localhost', '0.0.0.0') - - // Connect to WS output.browser = await puppeteer.connect({ browserWSEndpoint }) const pages = await output.browser.pages() output.page = pages[0] - - output.commoners = await output.page.evaluate(() => commoners.ready.then(() => commoners)) - console.log(output.commoners) } // Non-Electron Instance @@ -157,20 +159,17 @@ export const startBrowserTest = (customProps: Partial = {}, projectB const page = output.page = await browser.newPage(); await page.goto(url); output.page = page - output.commoners = await output.page.evaluate(() => commoners.ready.then(() => commoners)) - - } + output.commoners = await output.page.evaluate(() => commoners.ready.then(() => commoners)) + }) afterAll(async () => { - if (!toLaunch && isElectron && output.page) await output.page.evaluate(() => commoners.quit()).catch(e => { - console.log(e) - }) - if (output.browser) await output.browser.close() + if (output.browser) await output.browser.close() // Will also exit the Electron instance if (output.info.server) output.info.server.close() if (!toLaunch) afterStart(output.info) + }); return output @@ -209,56 +208,40 @@ export const checkAssets = (projectBase, baseDir = '', { build = false, target = if (!baseDir) baseDir = join(projectBase, globalTempDir) //---------------------- Vite ---------------------- - describe('Vite assets', () => { - - expect(existsSync(join(baseDir, 'assets'))).toBe(build) - - }) + expect(existsSync(join(baseDir, 'assets'))).toBe(build) // ---------------------- Common ---------------------- - describe('Common assets', () => { - - expect(existsSync(join(baseDir, 'commoners.config.mjs'))).toBe(true) - expect(existsSync(join(baseDir, 'commoners.config.cjs'))).toBe(true) - expect(existsSync(join(baseDir, 'onload.mjs'))).toBe(true) - expect(existsSync(join(baseDir, 'package.json'))).toBe(true) // Auto-generated package.json - expect(existsSync(join(baseDir, templateDir, 'icon.png'))).toBe(true) // Template icon - - }) - + expect(existsSync(join(baseDir, 'commoners.config.mjs'))).toBe(true) + expect(existsSync(join(baseDir, 'commoners.config.cjs'))).toBe(true) + expect(existsSync(join(baseDir, 'onload.mjs'))).toBe(true) + expect(existsSync(join(baseDir, 'package.json'))).toBe(true) // Auto-generated package.json + expect(existsSync(join(baseDir, templateDir, 'icon.png'))).toBe(true) // Template icon + // ---------------------- Electron ---------------------- const isElectron = target === 'electron' - describe('Electron assets', () => { - expect(existsSync(join(baseDir, 'main.js'))).toBe(isElectron) - expect(existsSync(join(baseDir, 'preload.js'))).toBe(isElectron) - expect(existsSync(join(baseDir, demoDir, 'splash.html'))).toBe(isElectron) - expect(existsSync(join(baseDir, '.env'))).toBe(isElectron) - }) + expect(existsSync(join(baseDir, 'main.js'))).toBe(isElectron) + expect(existsSync(join(baseDir, 'preload.js'))).toBe(isElectron) + expect(existsSync(join(baseDir, demoDir, 'splash.html'))).toBe(isElectron) + expect(existsSync(join(baseDir, '.env'))).toBe(isElectron) // Service - describe('Service assets', () => { + expect(existsSync(join(baseDir, '..', '..', 'services', 'http', 'http'))).toBe(isElectron) + expect(existsSync(join(baseDir, '..', '..', 'services', 'express', 'express'))).toBe(isElectron) - expect(existsSync(join(baseDir, '..', '..', 'services', 'http', 'http'))).toBe(isElectron) - expect(existsSync(join(baseDir, '..', '..', 'services', 'express', 'express'))).toBe(isElectron) - - // Custom with extra assets - expect(existsSync(join(baseDir, '..', '..', '..', 'build', 'manual', 'manual'))).toBe(isElectron) - - const txtFile = join(baseDir, '..', '..', '..', 'build', 'manual', 'test.txt') - expect(existsSync(txtFile)).toBe(isElectron) - // expect(readFileSync(txtFile, 'utf-8')).toBe('Hello world!') + // Custom with extra assets + expect(existsSync(join(baseDir, '..', '..', '..', 'build', 'manual', 'manual'))).toBe(isElectron) + + const txtFile = join(baseDir, '..', '..', '..', 'build', 'manual', 'test.txt') + expect(existsSync(txtFile)).toBe(isElectron) + if (isElectron && build) expect(readFileSync(txtFile, 'utf-8')).toBe('Hello world!') - }) // ---------------------- PWA ---------------------- - describe('PWA assets', () => { - - const isPWA = target === 'pwa' - expect(existsSync(join(baseDir, 'manifest.webmanifest'))).toBe(isPWA) - expect(existsSync(join(baseDir, 'registerSW.js'))).toBe(isPWA) - expect(existsSync(join(baseDir, 'sw.js'))).toBe(isPWA) - }) + const isPWA = target === 'pwa' + expect(existsSync(join(baseDir, 'manifest.webmanifest'))).toBe(isPWA) + expect(existsSync(join(baseDir, 'registerSW.js'))).toBe(isPWA) + expect(existsSync(join(baseDir, 'sw.js'))).toBe(isPWA) } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index cb9bfd6..422be22 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,5 +4,6 @@ import { defineConfig } from 'vite' export default defineConfig({ test: { // testTimeout: 50000, + // threads: false }, }) \ No newline at end of file