• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import { ArktsConfigBuilder, BuildConfig, CompileFileInfo, MockArktsConfigBuilder, ModuleInfo } from './artkts-config';
17import { MockPluginDriver, PluginDriver, stateName } from './plugin-driver';
18import { isNumber } from './safe-types';
19import {
20    canProceedToState,
21    destroyConfig,
22    destroyContext,
23    initGlobal,
24    resetConfig,
25    resetContext,
26} from './global';
27import { insertPlugin } from './compile';
28import { PluginExecutor, Plugins, PluginState } from '../../common/plugin-context';
29import { TesterCache } from './cache';
30import * as arkts from '@koalaui/libarkts';
31import * as fs from 'fs';
32
33type TestParams = Parameters<typeof test>;
34
35type SkipFirstParam<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : never;
36
37type PluginTestHooks = {
38    [K in PluginState | `${PluginState}:${string}`]?: SkipFirstParam<TestParams>;
39};
40
41type TestHooks = {
42    beforeAll?: Parameters<jest.Lifecycle>;
43    beforeEach?: Parameters<jest.Lifecycle>;
44    afterEach?: Parameters<jest.Lifecycle>;
45};
46
47export interface PluginTestContext {
48    scriptSnapshot?: string;
49    errors?: string[];
50    warnings?: string[];
51}
52
53export interface PluginTesterOptions {
54    stopAfter: PluginState;
55    buildConfig?: BuildConfig;
56}
57
58class PluginTester {
59    private configBuilder: ArktsConfigBuilder;
60    private pluginDriver: PluginDriver;
61    private describe: string;
62    private cache: TesterCache<PluginTestContext>;
63
64    constructor(describe: string, buildConfig?: BuildConfig) {
65        this.describe = describe;
66        this.configBuilder = new MockArktsConfigBuilder(buildConfig);
67        this.pluginDriver = new MockPluginDriver();
68        this.cache = TesterCache.getInstance();
69    }
70
71    private loadPluginDriver(plugins: Plugins[]): void {
72        this.pluginDriver.initPlugins(plugins);
73    }
74
75    private test(
76        key: PluginState | `${PluginState}:${string}`,
77        index: arkts.Es2pandaContextState,
78        testName: string,
79        pluginHooks: PluginTestHooks,
80        plugin?: PluginExecutor
81    ): void {
82        let cached: boolean = false;
83        const cacheKey: string = `${testName}-${key}`;
84        if (index > arkts.Es2pandaContextState.ES2PANDA_STATE_CHECKED) {
85            return;
86        }
87        if (canProceedToState(index)) {
88            arkts.proceedToState(index);
89        }
90        if (plugin) {
91            insertPlugin(this.pluginDriver, plugin, index);
92            this.captureContext(cacheKey);
93            cached = true;
94        }
95        const hook: SkipFirstParam<TestParams> | undefined = pluginHooks[key];
96        if (!!hook) {
97            if (!cached) this.captureContext(cacheKey);
98            test(testName, hook[0]?.bind(this.cache.get(cacheKey)), hook[1]);
99        }
100    }
101
102    private captureContext(cacheKey: string): void {
103        try {
104            // TODO: add error/warning handling after plugin
105            const context: PluginTestContext = this.cache.get(cacheKey) ?? {};
106            const script: arkts.EtsScript = arkts.EtsScript.fromContext();
107            context.scriptSnapshot = script.dumpSrc();
108            this.cache.set(cacheKey, context);
109        } catch (e) {
110            // Do nothing
111        }
112    }
113
114    private proceedToState(
115        state: PluginState,
116        index: arkts.Es2pandaContextState,
117        testName: string,
118        pluginHooks: PluginTestHooks,
119        plugins?: PluginExecutor[]
120    ): void {
121        if (plugins && plugins.length > 0) {
122            plugins.forEach((plugin) => {
123                const pluginName: string = plugin.name;
124                const key: `${PluginState}:${string}` = `${state}:${pluginName}`;
125                this.test(key, index, `[${key}] ${testName}`, pluginHooks, plugin);
126            });
127        }
128        this.test(state, index, `[${state}] ${testName}`, pluginHooks);
129    }
130
131    private singleFileCompile(
132        fileInfo: CompileFileInfo,
133        moduleInfo: ModuleInfo,
134        testName: string,
135        pluginHooks: PluginTestHooks,
136        stopAfter: PluginState
137    ): void {
138        let shouldStop: boolean = false;
139
140        Object.values(arkts.Es2pandaContextState)
141            .filter(isNumber)
142            .forEach((it) => {
143                if (shouldStop) {
144                    return;
145                }
146                const state: PluginState = stateName(it);
147                const plugins: PluginExecutor[] | undefined = this.pluginDriver.getSortedPlugins(it);
148                this.proceedToState(
149                    state,
150                    it,
151                    `${moduleInfo.packageName} - ${fileInfo.fileName}: ${testName}`,
152                    pluginHooks,
153                    plugins
154                );
155                shouldStop = state === stopAfter;
156            });
157    }
158
159    private traverseFile(testName: string, pluginHooks: PluginTestHooks, stopAfter: PluginState): void {
160        let once: boolean = false;
161        this.configBuilder.moduleInfos.forEach((moduleInfo) => {
162            moduleInfo.compileFileInfos.forEach((fileInfo) => {
163                if (!once) {
164                    initGlobal(fileInfo, this.configBuilder.isDebug);
165                    once = true;
166                } else {
167                    const source: string = fs.readFileSync(fileInfo.filePath).toString();
168                    resetContext(source);
169                }
170                this.singleFileCompile(fileInfo, moduleInfo, testName, pluginHooks, stopAfter);
171            });
172        });
173    }
174
175    run(
176        testName: string,
177        plugins: Plugins[],
178        pluginHooks: PluginTestHooks,
179        options: PluginTesterOptions,
180        testHooks?: TestHooks
181    ): void {
182        if (!!options.buildConfig) {
183            this.configBuilder = new MockArktsConfigBuilder(options.buildConfig);
184        }
185
186        this.cache.clear();
187        this.loadPluginDriver(plugins);
188
189        const that = this;
190        describe(this.describe, () => {
191            if (testHooks?.beforeAll) {
192                beforeAll(...testHooks.beforeAll);
193            }
194            if (testHooks?.beforeEach) {
195                beforeEach(...testHooks.beforeEach);
196            }
197            if (testHooks?.afterEach) {
198                afterEach(...testHooks.afterEach);
199            }
200            afterAll(() => {
201                destroyContext();
202                destroyConfig();
203            });
204            that.traverseFile(testName, pluginHooks, options.stopAfter);
205        });
206    }
207}
208
209export { PluginTester };
210