blob: d21c913215e9b740d04451d39f3eb9fbee0d9621 [file] [log] [blame]
/**
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you 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.
**/
const fs = require('node:fs');
const path = require('node:path');
const timers = require('node:timers/promises');
const et = require('elementtree');
const HooksRunner = require('../src/hooks/HooksRunner');
const cordova = require('../src/cordova/cordova');
const { tmpDir } = require('../spec/helpers');
const { PluginInfo, ConfigParser } = require('cordova-common');
const ext = process.platform === 'win32' ? 'bat' : 'sh';
const fixtures = path.join(__dirname, '../spec/cordova/fixtures');
describe('HooksRunner', function () {
let tmp, project, hooksRunner;
// This prepares a project that we will copy and use for all tests
beforeEach(() => {
tmp = tmpDir('hooks_test');
project = path.join(tmp, 'project');
// Copy base project fixture
fs.cpSync(path.join(fixtures, 'basePkgJson'), project, { recursive: true });
// Copy project hooks
const hooksDir = path.join(fixtures, 'projectHooks');
fs.cpSync(hooksDir, path.join(project, 'scripts'), { recursive: true });
// Change into our project directory
process.chdir(project);
process.env.PWD = project; // this is used by cordovaUtil.isCordova
hooksRunner = new HooksRunner(project);
});
afterEach(() => {
process.chdir(path.join(__dirname, '..')); // Non e2e tests assume CWD is repo root.
fs.rmSync(tmp, { recursive: true, force: true });
});
it('Test 001 : should throw if provided directory is not a cordova project', function () {
expect(_ => new HooksRunner(tmp)).toThrow();
});
it('Test 002 : should not throw if provided directory is a cordova project', function () {
expect(_ => new HooksRunner(project)).not.toThrow();
});
describe('fire method', function () {
const test_event = 'before_build';
let hooksOrderFile;
beforeEach(function () {
hooksOrderFile = path.join(project, 'hooks_order.txt');
fs.rmSync(hooksOrderFile, { recursive: true, force: true });
});
// helper methods
function getActualHooksOrder () {
const fileContents = fs.readFileSync(hooksOrderFile, 'ascii');
return fileContents.match(/\d+/g).map(Number);
}
function checkHooksOrderFile () {
expect(hooksOrderFile).toExist();
const hooksOrder = getActualHooksOrder();
const sortedHooksOrder = hooksOrder.slice(0).sort((a, b) => a - b);
expect(hooksOrder).toEqual(sortedHooksOrder);
}
function addHooks (hooksXml, doc) {
const hooks = et.parse(hooksXml);
for (const el of hooks.getroot().findall('./*')) {
doc.getroot().append(el);
}
}
describe('application hooks', function () {
const BASE_HOOKS = `
<widget xmlns="http://www.w3.org/ns/widgets">
<hook type="before_build" src="scripts/appBeforeBuild1.${ext}" />
<hook type="before_build" src="scripts/appBeforeBuild02.js" />
</widget>
`;
const WINDOWS_HOOKS = `
<widget xmlns="http://www.w3.org/ns/widgets">
<platform name="windows">
<hook type="before_build" src="scripts/windows/appWindowsBeforeBuild.${ext}" />
<hook type="before_build" src="scripts/windows/appWindowsBeforeBuild.js" />
</platform>
</widget>
`;
const ANDROID_HOOKS = `
<widget xmlns="http://www.w3.org/ns/widgets">
<platform name="android">
<hook type="before_build" src="scripts/android/appAndroidBeforeBuild.${ext}" />
<hook type="before_build" src="scripts/android/appAndroidBeforeBuild.js" />
</platform>
</widget>
`;
function addHooksToConfig (hooksXml) {
const config = new ConfigParser(path.join(project, 'config.xml'));
addHooks(hooksXml, config.doc);
config.write();
}
it('Test 006 : should execute hook scripts serially from config.xml', function () {
addHooksToConfig(BASE_HOOKS);
return hooksRunner.fire(test_event)
.then(checkHooksOrderFile);
});
it('Test 007 : should execute hook scripts serially from config.xml including platform scripts', function () {
addHooksToConfig(BASE_HOOKS);
addHooksToConfig(WINDOWS_HOOKS);
return hooksRunner.fire(test_event)
.then(checkHooksOrderFile);
});
it('Test 008 : should filter hook scripts from config.xml by platform', function () {
addHooksToConfig(BASE_HOOKS);
addHooksToConfig(WINDOWS_HOOKS);
addHooksToConfig(ANDROID_HOOKS);
const hookOptions = { cordova: { platforms: ['android'] } };
return hooksRunner.fire(test_event, hookOptions).then(function () {
checkHooksOrderFile();
const baseScriptResults = [8, 9];
const androidPlatformScriptsResults = [14, 15];
const expectedResults = baseScriptResults.concat(androidPlatformScriptsResults);
expect(getActualHooksOrder()).toEqual(expectedResults);
});
});
it('Test 023 : should error if any hook fails', function () {
const FAIL_HOOK = `
<widget xmlns="http://www.w3.org/ns/widgets">
<hook type="fail" src="scripts/fail.js" />
</widget>
`;
addHooksToConfig(FAIL_HOOK);
return expectAsync(
hooksRunner.fire('fail')
).toBeRejectedWithError();
});
it('Test 024 : should not error if the hook is unrecognized', function () {
return hooksRunner.fire('CLEAN YOUR SHORTS GODDAMNIT LIKE A BIG BOY!');
});
});
describe('plugin hooks', function () {
const PLUGIN_BASE_HOOKS = `
<widget xmlns="http://www.w3.org/ns/widgets">
<hook type="before_build" src="scripts/beforeBuild.js" />
<hook type="before_build" src="scripts/beforeBuild.${ext}" />
</widget>
`;
const PLUGIN_WINDOWS_HOOKS = `
<widget xmlns="http://www.w3.org/ns/widgets">
<platform name="windows">
<hook type="before_build" src="scripts/windows/windowsBeforeBuild.js" />
</platform>
</widget>
`;
const PLUGIN_ANDROID_HOOKS = `
<widget xmlns="http://www.w3.org/ns/widgets">
<platform name="android">
<hook type="before_build" src="scripts/android/androidBeforeBuild.js" />
</platform>
</widget>
`;
const testPlugin = 'com.plugin.withhooks';
const testPluginFixture = path.join(fixtures, 'plugins', testPlugin);
let testPluginInstalledPath;
beforeEach(() => {
// Add the test plugin to our project
testPluginInstalledPath = path.join(project, 'plugins', testPlugin);
fs.cpSync(testPluginFixture, testPluginInstalledPath, { recursive: true });
});
function addHooksToPlugin (hooksXml) {
const config = new PluginInfo(testPluginInstalledPath);
addHooks(hooksXml, config._et);
const configPath = path.join(testPluginInstalledPath, 'plugin.xml');
fs.writeFileSync(configPath, config._et.write({ indent: 4 }));
}
it('Test 009 : should execute hook scripts serially from plugin.xml', function () {
addHooksToPlugin(PLUGIN_BASE_HOOKS);
return hooksRunner.fire(test_event)
.then(checkHooksOrderFile);
});
it('Test 010 : should execute hook scripts serially from plugin.xml including platform scripts', function () {
addHooksToPlugin(PLUGIN_BASE_HOOKS);
addHooksToPlugin(PLUGIN_WINDOWS_HOOKS);
return hooksRunner.fire(test_event)
.then(checkHooksOrderFile);
});
it('Test 011 : should filter hook scripts from plugin.xml by platform', function () {
addHooksToPlugin(PLUGIN_BASE_HOOKS);
addHooksToPlugin(PLUGIN_WINDOWS_HOOKS);
addHooksToPlugin(PLUGIN_ANDROID_HOOKS);
const hookOptions = { cordova: { platforms: ['android'] } };
return hooksRunner.fire(test_event, hookOptions).then(function () {
checkHooksOrderFile();
const baseScriptResults = [21, 22];
const androidPlatformScriptsResults = [26];
const expectedResults = baseScriptResults.concat(androidPlatformScriptsResults);
expect(getActualHooksOrder()).toEqual(expectedResults);
});
});
});
describe('nohooks option', () => {
it('Test 013 : should not execute the designated hook when --nohooks option specifies the exact hook name', async () => {
const hookOptions = { nohooks: [test_event] };
expect(await hooksRunner.fire(test_event, hookOptions))
.toBe('hook before_build is disabled.');
});
it('Test 014 : should not execute matched hooks when --nohooks option specifies a hook pattern', async () => {
const hookOptions = { nohooks: ['ba'] };
for (const e of ['foo', 'bar', 'baz']) {
expect(await hooksRunner.fire(e, hookOptions))
.toBe(e === 'foo' ? undefined : `hook ${e} is disabled.`);
}
});
it('Test 015 : should not execute any hooks when --nohooks option specifies .', async () => {
const hookOptions = { nohooks: ['.'] };
for (const e of ['foo', 'bar', 'baz']) {
expect(await hooksRunner.fire(e, hookOptions))
.toBe(`hook ${e} is disabled.`);
}
});
});
describe('module-level hooks (event handlers)', function () {
let handler;
beforeEach(() => {
handler = jasmine.createSpy('testHandler').and.resolveTo();
cordova.on(test_event, handler);
});
afterEach(function () {
cordova.removeAllListeners(test_event);
});
it('Test 016 : should fire handlers that were attached using cordova.on', function () {
return hooksRunner.fire(test_event).then(function () {
expect(handler).toHaveBeenCalled();
});
});
it('Test 017 : should pass the project root folder as parameter into the module-level handlers', function () {
return hooksRunner.fire(test_event).then(function () {
expect(handler).toHaveBeenCalledWith(jasmine.objectContaining({
projectRoot: project
}));
});
});
it('Test 018 : should be able to stop listening to events using cordova.off', function () {
cordova.off(test_event, handler);
return hooksRunner.fire(test_event).then(function () {
expect(handler).not.toHaveBeenCalled();
});
});
it('Test 019 : should execute async event listeners serially', function () {
const order = [];
// Delay 100 ms here to check that h2 is not executed until after
// the promise returned by h1 is resolved.
const h1 = _ => timers.setTimeout(100).then(_ => order.push(1));
const h2 = _ => Promise.resolve().then(_ => order.push(2));
cordova.on(test_event, h1);
cordova.on(test_event, h2);
return hooksRunner.fire(test_event)
.then(_ => expect(order).toEqual([1, 2]));
});
it('Test 021 : should pass options passed to fire into handlers', async () => {
const hookOptions = { test: 'funky' };
await hooksRunner.fire(test_event, hookOptions);
expect(handler).toHaveBeenCalledWith(hookOptions);
});
});
});
});