import { test } from 'brittle'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import { scan } from '../lib/scanner.js'
const HERE = dirname(fileURLToPath(import.meta.url))
// Was `resolve(HERE, '../../')` — the live hyper-fleeky tree, which put five
// assertions here at the mercy of a sibling repo's version bump (invariant I1).
const FIXTURE = resolve(HERE, 'fixtures/workspace')
test('tree: single module', async (t) => {
const tree = await scan(resolve(FIXTURE, 'widget-store'))
t.ok(tree.generated, 'has timestamp')
t.ok(tree.root, 'has root path')
// System level
t.ok(tree.system.modules.includes('widget-store'), 'module listed')
t.ok(tree.system.dependencies['widget-store'], 'has dependencies')
// Module level
const mod = tree.modules['widget-store']
t.ok(mod, 'module entry exists')
t.is(mod.name, 'widget-store', 'correct name')
t.is(mod.version, '0.2.0', 'correct version')
t.is(mod.type, 'module', 'ESM')
t.is(mod.entry, 'index.js', 'entry point')
t.is(mod.testCommand, 'brittle test/*.test.js', 'has test command')
// Public API comes from the entry file's exports
t.alike(mod.publicAPI, ['WidgetStore', 'storeSchema', 'RESERVED_NAMES', 'validateName'],
'public API is the barrel, in source order')
// Files
t.ok(mod.files['index.js'], 'has index.js')
t.ok(mod.files['lib/widget-store.js'], 'has lib/widget-store.js')
t.ok(mod.files['lib/schema.js'], 'has lib/schema.js')
})
test('tree: file-level detail', async (t) => {
const tree = await scan(resolve(FIXTURE, 'widget-store'))
const file = tree.modules['widget-store'].files['lib/widget-store.js']
// Imports
t.is(file.imports.length, 2, '2 imports')
const roomImport = file.imports.find(i => i.specifier === 'widget-rooms')
t.ok(roomImport, 'imports widget-rooms')
// Exports
t.is(file.exports.length, 1, '1 export')
t.is(file.exports[0].name, 'WidgetStore', 'exports WidgetStore')
t.is(file.exports[0].type, 'class', 'type is class')
// Class structure
t.is(file.classes.length, 1, '1 class')
const cls = file.classes[0]
t.is(cls.name, 'WidgetStore', 'class name')
t.ok(cls.constructor, 'has constructor')
t.is(cls.methods.length, 13, '13 methods')
t.is(cls.getters.length, 6, '6 getters')
// State and delegation survive into the tree, not just the parser
t.ok(file.state.includes('_room'), 'state includes _room')
t.ok(file.delegates.find(d => d.field === '_room'), 'delegates to _room')
// Error contracts
t.is(file.errorContracts.length, 2, '2 error contracts')
})
test('tree: multi-module scan with shared deps', async (t) => {
const tree = await scan(FIXTURE, { maxDepth: 1 })
t.is(tree.system.modules.length, 4, '4 modules')
// A shared dep is one used by 2+ modules — here b4a, by both ESM packages.
t.alike(tree.system.sharedDeps, { b4a: ['widget-rooms', 'widget-store'] },
'b4a is the shared dep, with both users named')
// Parsed file data survives a multi-module scan (it once did not)
t.ok(tree.modules['widget-store'].files['lib/widget-store.js'].classes,
'widget-store has parsed class data')
t.ok(tree.modules['widget-rooms'].publicAPI.includes('Room'),
'widget-rooms exposes Room')
})
test('tree: dependency graph is populated', async (t) => {
const tree = await scan(FIXTURE, { maxDepth: 1 })
// No `if (deps)` guard any more: the fixture cannot stop depending on this.
t.alike(tree.system.dependencies['widget-store'], ['widget-rooms', 'b4a'],
'widget-store -> widget-rooms, b4a')
t.alike(tree.system.dependencies['widget-rooms'], ['b4a'],
'widget-rooms -> b4a')
})
test('tree: file roles are correctly classified', async (t) => {
const tree = await scan(resolve(FIXTURE, 'widget-store'))
const files = tree.modules['widget-store'].files
t.is(files['index.js'].role, 'entry', 'index.js = entry')
t.is(files['lib/widget-store.js'].role, 'lib', 'lib/*.js = lib')
t.is(files['lib/schema.js'].role, 'schema', 'anything named schema = schema')
t.is(files['lib/handlers/widgets.js'].role, 'lib', 'nested under lib/ = lib')
t.is(files['test/store.test.js'].role, 'test', 'test/*.test.js = test')
})