mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 09:37:22 +00:00
refactor(mock): generate @podman-desktop/api mock from its definition (#15670)
* refactor(mock): generate @podman-desktop/api mock from its definition instead of writing for each function and for each namespace the content reuse the .d.ts definition and wire automatically vi.fn() customize/override some classes implementation manually but most of it is generated fixes https://github.com/podman-desktop/podman-desktop/issues/14493 Signed-off-by: Florent Benoit <fbenoit@redhat.com>
This commit is contained in:
parent
cfc879fbf4
commit
1bc01dc186
15 changed files with 208 additions and 170 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -15,3 +15,4 @@ dist
|
|||
yarn.lock
|
||||
extensions/podman/assets/
|
||||
extensions-extra/
|
||||
__mocks__/@podman-desktop/api.ts
|
||||
|
|
|
|||
|
|
@ -1,170 +0,0 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2022-2025 Red Hat, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import { vi, beforeEach } from 'vitest';
|
||||
import product from './../../product.json' with { type: 'json' };
|
||||
|
||||
/**
|
||||
* Mock the extension API for vitest.
|
||||
* This file is referenced from vitest.config.js file.
|
||||
*/
|
||||
|
||||
const cli = {
|
||||
createCliTool: vi.fn(),
|
||||
};
|
||||
|
||||
const commands = {
|
||||
registerCommand: vi.fn(),
|
||||
};
|
||||
|
||||
const configuration = {
|
||||
onDidChangeConfiguration: vi.fn(),
|
||||
getConfiguration: vi.fn(),
|
||||
};
|
||||
|
||||
const containerEngine = {
|
||||
info: vi.fn(),
|
||||
listContainers: vi.fn(),
|
||||
saveImage: vi.fn(),
|
||||
onEvent: vi.fn(),
|
||||
};
|
||||
|
||||
const context = {
|
||||
setValue: vi.fn(),
|
||||
};
|
||||
|
||||
const env = {
|
||||
createTelemetryLogger: vi.fn(),
|
||||
openExternal: vi.fn(),
|
||||
|
||||
appName: product.name,
|
||||
|
||||
isLinux: false,
|
||||
isWindows: false,
|
||||
isMac: false,
|
||||
};
|
||||
|
||||
const kubernetes = {
|
||||
createResources: vi.fn(),
|
||||
getKubeconfig: vi.fn(),
|
||||
onDidUpdateKubeconfig: vi.fn(),
|
||||
};
|
||||
|
||||
const net = {
|
||||
getFreePort: vi.fn(),
|
||||
};
|
||||
|
||||
const process = {
|
||||
exec: vi.fn(),
|
||||
};
|
||||
|
||||
const eventEmitterListeners = [];
|
||||
|
||||
const extensions = {
|
||||
getExtension: vi.fn(),
|
||||
};
|
||||
|
||||
const proxy = {
|
||||
isEnabled: vi.fn(),
|
||||
onDidUpdateProxy: vi.fn(),
|
||||
onDidStateChange: vi.fn(),
|
||||
getProxySettings: vi.fn(),
|
||||
};
|
||||
|
||||
const provider = {
|
||||
createProvider: vi.fn(),
|
||||
onDidRegisterContainerConnection: vi.fn(),
|
||||
onDidUnregisterContainerConnection: vi.fn(),
|
||||
onDidUpdateProvider: vi.fn(),
|
||||
onDidUpdateContainerConnection: vi.fn(),
|
||||
onDidUpdateVersion: vi.fn(),
|
||||
registerUpdate: vi.fn(),
|
||||
getContainerConnections: vi.fn(),
|
||||
};
|
||||
|
||||
const registry = {
|
||||
registerRegistryProvider: vi.fn(),
|
||||
registerRegistry: vi.fn(),
|
||||
unregisterRegistry: vi.fn(),
|
||||
onDidRegisterRegistry: vi.fn(),
|
||||
onDidUnregisterRegistry: vi.fn(),
|
||||
onDidUpdateRegistry: vi.fn(),
|
||||
suggestRegistry: vi.fn(),
|
||||
};
|
||||
|
||||
const window = {
|
||||
showInformationMessage: vi.fn(),
|
||||
showErrorMessage: vi.fn(),
|
||||
withProgress: vi.fn(),
|
||||
showNotification: vi.fn(),
|
||||
showWarningMessage: vi.fn(),
|
||||
|
||||
showQuickPick: vi.fn(),
|
||||
showInputBox: vi.fn(),
|
||||
createStatusBarItem: vi.fn(),
|
||||
};
|
||||
|
||||
const Disposable = { from: vi.fn(), dispose: vi.fn() };
|
||||
|
||||
class EventEmitter {
|
||||
event(callback) {
|
||||
eventEmitterListeners.push(callback);
|
||||
}
|
||||
|
||||
fire(data) {
|
||||
eventEmitterListeners.forEach(listener => listener(data));
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
}
|
||||
|
||||
const ProgressLocation = {
|
||||
APP_ICON: 1,
|
||||
TASK_WIDGET: 2,
|
||||
};
|
||||
|
||||
const Uri = {
|
||||
parse: vi.fn(),
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
cli,
|
||||
commands,
|
||||
configuration,
|
||||
containerEngine,
|
||||
context,
|
||||
env,
|
||||
extensions,
|
||||
kubernetes,
|
||||
net,
|
||||
process,
|
||||
provider,
|
||||
proxy,
|
||||
registry,
|
||||
window,
|
||||
Disposable,
|
||||
EventEmitter,
|
||||
ProgressLocation,
|
||||
Uri,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
eventEmitterListeners.length = 0;
|
||||
});
|
||||
|
||||
module.exports = plugin;
|
||||
69
__mocks__/api.mustache
Normal file
69
__mocks__/api.mustache
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2026 Red Hat, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
/* This file is auto-generated by a vitest globalSetup hook */
|
||||
import { beforeEach, vi } from 'vitest';
|
||||
|
||||
import product from '../../product.json' with { type: 'json' };
|
||||
|
||||
const namespaceMocks = {} as Record<string, Record<string, unknown>>;
|
||||
{{#namespaces}}
|
||||
namespaceMocks.{{name}} = {};
|
||||
{{#functions}}
|
||||
namespaceMocks.{{name}}.{{.}} = vi.fn();
|
||||
{{/functions}}
|
||||
{{/namespaces}}
|
||||
|
||||
const classesMocks = {} as Record<string, unknown>;
|
||||
{{#classes}}
|
||||
const {{name}}Instance: Record<string, unknown> = {};
|
||||
{{#methods}}
|
||||
{{name}}Instance.{{.}} = vi.fn();
|
||||
{{/methods}}
|
||||
classesMocks.{{name}} = {{name}}Instance;
|
||||
{{/classes}}
|
||||
|
||||
const eventEmitterListeners: Array<(data: unknown) => unknown> = [];
|
||||
beforeEach(() => { eventEmitterListeners.length = 0; });
|
||||
class EventEmitter {
|
||||
event(callback: (e: unknown) => unknown): void { eventEmitterListeners.push(callback); }
|
||||
fire(data: unknown): void { for (const listener of eventEmitterListeners) { listener(data); } }
|
||||
dispose(): void {}
|
||||
}
|
||||
EventEmitter.prototype.dispose = vi.fn();
|
||||
|
||||
// Normalize env mock
|
||||
if (!namespaceMocks.env) namespaceMocks.env = {};
|
||||
namespaceMocks.env.isWindows = false;
|
||||
namespaceMocks.env.isMac = false;
|
||||
namespaceMocks.env.isLinux = false;
|
||||
namespaceMocks.env.appName = product.name;
|
||||
|
||||
const extensionAPI = {
|
||||
...namespaceMocks,
|
||||
...classesMocks,
|
||||
EventEmitter,
|
||||
ProgressLocation: { APP_ICON: 1, TASK_WIDGET: 2 },
|
||||
StatusBarAlignLeft: 'LEFT',
|
||||
StatusBarAlignRight: 'RIGHT',
|
||||
StatusBarItemDefaultPriority: 0,
|
||||
InputBoxValidationSeverity: { Info: 1, Warning: 2, Error: 3 },
|
||||
QuickPickItemKind: { Separator: -1, Default: 0 },
|
||||
};
|
||||
|
||||
module.exports = extensionAPI;
|
||||
125
__mocks__/vitest-generate-api-global-setup.ts
Normal file
125
__mocks__/vitest-generate-api-global-setup.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/**********************************************************************
|
||||
* Copyright (C) 2026 Red Hat, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import typescript from 'typescript';
|
||||
import Mustache from 'mustache';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
type Namespaces = { [key: string]: string[] };
|
||||
type Classes = { [key: string]: string[] };
|
||||
|
||||
async function extractNamespacesAndClassesFromAPI(
|
||||
filePath: string,
|
||||
): Promise<{ namespaces: Namespaces; classes: Classes }> {
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const sourceFile = typescript.createSourceFile(filePath, fileContent, typescript.ScriptTarget.Latest, true);
|
||||
|
||||
const namespaces: Namespaces = {};
|
||||
const classes: Classes = {};
|
||||
|
||||
const visit = (node: typescript.Node): void => {
|
||||
if (typescript.isModuleDeclaration(node) && node.name.text) {
|
||||
if (node.name.text === '@podman-desktop/api') {
|
||||
typescript.forEachChild(node, visit);
|
||||
return;
|
||||
}
|
||||
|
||||
const namespaceName = node.name.text;
|
||||
const functions: string[] = [];
|
||||
|
||||
if (node.body && typescript.isModuleBlock(node.body)) {
|
||||
for (const statement of node.body.statements) {
|
||||
if (typescript.isFunctionDeclaration(statement) && statement.name) {
|
||||
functions.push(statement.name.text);
|
||||
} else if (typescript.isVariableStatement(statement)) {
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
if (typescript.isIdentifier(declaration.name)) {
|
||||
functions.push(declaration.name.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (functions.length > 0) {
|
||||
// Deduplicate to handle function overloads
|
||||
namespaces[namespaceName] = Array.from(new Set(functions));
|
||||
}
|
||||
}
|
||||
|
||||
if (typescript.isClassDeclaration(node) && node.name) {
|
||||
const modifiers = typescript.getModifiers(node);
|
||||
const isExported = modifiers?.some(m => m.kind === typescript.SyntaxKind.ExportKeyword);
|
||||
if (isExported) {
|
||||
const className = node.name.text;
|
||||
const methods: string[] = [];
|
||||
for (const member of node.members) {
|
||||
if (typescript.isMethodDeclaration(member) && member.name && typescript.isIdentifier(member.name)) {
|
||||
methods.push(member.name.text);
|
||||
}
|
||||
}
|
||||
// Deduplicate to handle method overloads
|
||||
classes[className] = Array.from(new Set(methods));
|
||||
}
|
||||
}
|
||||
|
||||
typescript.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
return { namespaces, classes };
|
||||
}
|
||||
|
||||
function toTemplateData(data: { namespaces: Namespaces; classes: Classes }) {
|
||||
const namespaces = Object.entries(data.namespaces).map(([name, functions]) => ({ name, functions }));
|
||||
const classes = Object.entries(data.classes).map(([name, methods]) => ({ name, methods }));
|
||||
return { namespaces, classes };
|
||||
}
|
||||
|
||||
export default async function setup(): Promise<void> {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const extensionApiTypePath = path.join(repoRoot, 'packages', 'extension-api', 'src', 'extension-api.d.ts');
|
||||
const podmanDesktopApiMocksDir = path.join(repoRoot, '__mocks__', '@podman-desktop');
|
||||
const apiGeneratedFile = path.join(podmanDesktopApiMocksDir, 'api.ts');
|
||||
const templatePath = path.join(repoRoot, '__mocks__', 'api.mustache');
|
||||
|
||||
// skip if api.ts is already newer (from template or extension-api.d.ts file)
|
||||
const extensionApiPathStats = await fs.stat(extensionApiTypePath);
|
||||
const templatePathStats = await fs.stat(templatePath);
|
||||
try {
|
||||
const outputStats = await fs.stat(apiGeneratedFile);
|
||||
const newestInputMtime = Math.max(extensionApiPathStats.mtimeMs, templatePathStats.mtimeMs);
|
||||
if (outputStats.mtimeMs >= newestInputMtime) {
|
||||
console.debug(' 🚀 __mocks__/@podman-desktop/api.ts up-to-date; skipping regeneration');
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
console.debug(' ⚙️ __mocks__/@podman-desktop/api.ts does not exist yet; generating it now…');
|
||||
}
|
||||
const data = await extractNamespacesAndClassesFromAPI(extensionApiTypePath);
|
||||
const template = await fs.readFile(templatePath, 'utf-8');
|
||||
const content = Mustache.render(template, toTemplateData(data));
|
||||
|
||||
await fs.mkdir(podmanDesktopApiMocksDir, { recursive: true });
|
||||
await fs.writeFile(apiGeneratedFile, content, 'utf-8');
|
||||
console.debug(' ✅ __mocks__/@podman-desktop/api.ts has been generated.');
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ const config = {
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
globalSetup: [join(PACKAGE_ROOT, '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')],
|
||||
alias: {
|
||||
'@podman-desktop/api': join(PACKAGE_ROOT, '..', '..', '__mocks__/@podman-desktop/api.js'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const config = {
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
globalSetup: [join(PACKAGE_ROOT, '..', '..', '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')],
|
||||
alias: {
|
||||
'@podman-desktop/api': join(PACKAGE_ROOT, '..', '..', '..', '..', '__mocks__/@podman-desktop/api.js'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const config = {
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
globalSetup: [join(PACKAGE_ROOT, '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')],
|
||||
alias: {
|
||||
'@podman-desktop/api': join(PACKAGE_ROOT, '..', '..', '__mocks__/@podman-desktop/api.js'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const config = {
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
globalSetup: [join(PACKAGE_ROOT, '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')],
|
||||
alias: {
|
||||
'@podman-desktop/api': join(PACKAGE_ROOT, '..', '..', '__mocks__/@podman-desktop/api.js'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ const config = {
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
globalSetup: [join(PACKAGE_ROOT, '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')],
|
||||
alias: {
|
||||
'@podman-desktop/api': join(PACKAGE_ROOT, '..', '..', '__mocks__/@podman-desktop/api.js'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const config = {
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
globalSetup: [join(PACKAGE_ROOT, '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')],
|
||||
alias: {
|
||||
'@podman-desktop/api': join(PACKAGE_ROOT, '..', '..', '__mocks__/@podman-desktop/api.js'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const config = {
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
globalSetup: [join(PACKAGE_ROOT, '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')],
|
||||
alias: {
|
||||
'@podman-desktop/api': join(PACKAGE_ROOT, '..', '..', '__mocks__/@podman-desktop/api.js'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ const config = {
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['{src,scripts}/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
globalSetup: [join(PACKAGE_ROOT, '..', '..', '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')],
|
||||
alias: {
|
||||
'@podman-desktop/api': join(PACKAGE_ROOT, '..', '..', '..', '..', '__mocks__/@podman-desktop/api.js'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const config = {
|
|||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
||||
globalSetup: [join(PACKAGE_ROOT, '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')],
|
||||
alias: {
|
||||
'@podman-desktop/api': join(PACKAGE_ROOT, '..', '..', '__mocks__/@podman-desktop/api.js'),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@
|
|||
"typescript-eslint": "^8.54.0",
|
||||
"validator": "^13.15.26",
|
||||
"vite": "^7.3.1",
|
||||
"mustache": "^4.2.0",
|
||||
"vitest": "^4.0.10",
|
||||
"xvfb-maybe": "^0.2.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -301,6 +301,9 @@ importers:
|
|||
msw:
|
||||
specifier: ^2.12.7
|
||||
version: 2.12.7(@types/node@24.6.2)(typescript@5.9.3)
|
||||
mustache:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
prettier:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1
|
||||
|
|
|
|||
Loading…
Reference in a new issue