Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/admin-jest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Administration Jest

on:
pull_request:
paths:
- 'src/Resources/app/administration/**'
- .github/workflows/admin-jest.yml
push:
branches:
- main
paths:
- 'src/Resources/app/administration/**'
- .github/workflows/admin-jest.yml
workflow_dispatch:

jobs:
jest:
strategy:
fail-fast: false
matrix:
shopware-version:
- '6.6.x'
- 'trunk'
uses: shopware/github-actions/.github/workflows/admin-jest.yml@main
with:
extensionName: ${{ github.event.repository.name }}
shopwareVersion: ${{ matrix.shopware-version }}
uploadCoverage: true
secrets:
codecovToken: ${{ secrets.CODECOV_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
/src/Resources/public/
/src/Resources/app/administration/node_modules/
/src/Resources/app/administration/.tmp/
/src/Resources/app/administration/build/
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ The current feature set consists of:
- basic view of order, transaction and delivery states
- **Override system config by config files**
- Overwrite any system config value with static or environment values
- **Command Palette**
- Open with `Ctrl/⌘ + K` (or the sidebar search control)
- Jump to any Tools tab and run common actions (clear caches, compile themes, reset queue, register tasks)

## Installation

Expand Down
32 changes: 32 additions & 0 deletions src/Resources/app/administration/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Plugin test config: reuses the Shopware administration's jest config and
* only points it at this plugin's spec files. This keeps the plugin on the
* exact same harness (transformers, Shopware globals, jsdom setup) that the
* Shopware installation provides, instead of duplicating it.
*/
const { join } = require('path');
const resolveAdminPath = require('./test/resolve-admin-path');

const adminPath = resolveAdminPath();

process.env.ADMIN_PATH = process.env.ADMIN_PATH || adminPath;
process.env.PROJECT_ROOT = process.env.PROJECT_ROOT || adminPath;

// eslint-disable-next-line import/no-dynamic-require
const coreConfig = require(join(adminPath, 'jest.config.js'));

module.exports = {
...coreConfig,
rootDir: adminPath,
roots: [join(__dirname, 'src')],
testMatch: [join(__dirname, 'src/**/*.spec.js')],
moduleNameMapper: {
...coreConfig.moduleNameMapper,
'^frosh-test/(.*)$': join(__dirname, 'test/$1'),
},
coverageDirectory: join(__dirname, 'build', 'artifacts', 'jest'),
collectCoverageFrom: [
join(__dirname, 'src/**/*.js'),
`!${join(__dirname, 'src/**/*.spec.js')}`,
],
};
4 changes: 4 additions & 0 deletions src/Resources/app/administration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"name": "Resources",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"unit": "node test/run.js",
"unit:watch": "node test/run.js --watch"
},
"dependencies": {
"diff-match-patch": "^1.0.5",
"lucide-static": "^1.14.0",
Expand Down
78 changes: 50 additions & 28 deletions src/Resources/app/administration/src/api/frosh-tools.spec.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import FroshTools from './frosh-tools';
import FroshToolsService from './frosh-tools';

describe('FroshTools queue API', () => {
/**
* The service is constructed the same way the DI container does it, with the
* HTTP client as the only mock — that is the system boundary. Base path and
* auth headers are the real implementation.
*/
describe('FroshTools API service', () => {
let httpClient;
let service;

beforeEach(() => {
service = Object.create(FroshTools.prototype);
service.httpClient = {
httpClient = {
get: jest.fn().mockResolvedValue({ data: {} }),
post: jest.fn().mockResolvedValue({ data: {} }),
delete: jest.fn().mockResolvedValue({ data: {} }),
};
service.getApiBasePath = () => '/_action/frosh-tools';
service.getBasicHeaders = () => ({});

service = new FroshToolsService(httpClient, {
getToken: () => 'test-token',
isLoggedIn: () => true,
});
});

it('encodes transport names in every queue route', async () => {
Expand All @@ -23,48 +31,62 @@ describe('FroshTools queue API', () => {
await service.deleteQueueMessage(name, 'message/id');
await service.purgeQueueTransport(name);

expect(service.httpClient.get).toHaveBeenCalledWith(
`/_action/frosh-tools/queue/transport/${encodedName}/messages`,
expect.any(Object)
expect(httpClient.get).toHaveBeenCalledWith(
`_action/frosh-tools/queue/transport/${encodedName}/messages`,
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer test-token',
}),
})
);
expect(service.httpClient.post).toHaveBeenCalledWith(
`/_action/frosh-tools/queue/transport/${encodedName}/messages/message%2Fid/retry`,
expect(httpClient.post).toHaveBeenCalledWith(
`_action/frosh-tools/queue/transport/${encodedName}/messages/message%2Fid/retry`,
{},
expect.any(Object)
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer test-token',
}),
})
);
expect(service.httpClient.delete).toHaveBeenNthCalledWith(
expect(httpClient.delete).toHaveBeenNthCalledWith(
1,
`/_action/frosh-tools/queue/transport/${encodedName}/messages/message%2Fid`,
expect.any(Object)
`_action/frosh-tools/queue/transport/${encodedName}/messages/message%2Fid`,
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer test-token',
}),
})
);
expect(service.httpClient.delete).toHaveBeenNthCalledWith(
expect(httpClient.delete).toHaveBeenNthCalledWith(
2,
`/_action/frosh-tools/queue/transport/${encodedName}`,
expect.any(Object)
`_action/frosh-tools/queue/transport/${encodedName}`,
expect.objectContaining({
headers: expect.objectContaining({
Authorization: 'Bearer test-token',
}),
})
);
});

it('requests the security SBOM as a blob attachment', async () => {
await service.getSecuritySbom();
await service.getSecuritySbom(true);

expect(service.httpClient.get).toHaveBeenNthCalledWith(
expect(httpClient.get).toHaveBeenNthCalledWith(
1,
'/_action/frosh-tools/security/sbom',
{
headers: {},
'_action/frosh-tools/security/sbom',
expect.objectContaining({
params: {},
responseType: 'blob',
}
})
);
expect(service.httpClient.get).toHaveBeenNthCalledWith(
expect(httpClient.get).toHaveBeenNthCalledWith(
2,
'/_action/frosh-tools/security/sbom',
{
headers: {},
'_action/frosh-tools/security/sbom',
expect.objectContaining({
params: { includeDev: 1 },
responseType: 'blob',
}
})
);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { mount } from '@vue/test-utils';
import '../../../../mixin/sortable-table';
import '../ft-modal';
import { mountFrosh } from 'frosh-test/mount';
import './index';

function createService() {
Expand All @@ -18,41 +16,40 @@ function createService() {
};
}

/**
* Mounts the tab with the real ft-* component tree (including the real
* ft-modal for the confirmation dialog) and real translations. Only the
* elasticsearch API service (the HTTP boundary) is faked; the Shopware
* core code editor stays stubbed.
*/
async function createWrapper(service) {
const [component, ftModal] = await Promise.all([
Shopware.Component.build('frosh-tools-tab-elasticsearch'),
Shopware.Component.build('ft-modal'),
]);

return mount(component, {
global: {
provide: { froshElasticSearch: service },
components: { 'ft-modal': ftModal },
stubs: [
'ft-page-head',
'ft-panel',
'ft-empty',
'ft-hero-state',
'ft-pill',
'ft-icon',
'ft-th-sort',
'ft-refresh-button',
'sw-code-editor',
'teleport',
],
mocks: {
$t: (key) => key,
$tc: (key) => key,
},
directives: {
tooltip: {},
},
},
return mountFrosh('frosh-tools-tab-elasticsearch', {
provide: { froshElasticSearch: service },
stubs: ['sw-code-editor'],
attachTo: document.body,
});
}

/**
* Success notifications are growl-only and never persisted; errors are
* persisted. Read both store slices to see what the user was shown.
*/
function notificationsOfVariant(variant) {
const store = Shopware.Store.get('notification');

return [
...Object.values(store.growlNotifications),
...Object.values(store.notifications),
].filter((notification) => notification.variant === variant);
}

describe('frosh-tools-tab-elasticsearch destructive actions', () => {
beforeEach(() => {
const store = Shopware.Store.get('notification');
store.growlNotifications = {};
store.notifications = {};
});

afterEach(() => {
document.body.innerHTML = '';
});
Expand All @@ -71,14 +68,10 @@ describe('frosh-tools-tab-elasticsearch destructive actions', () => {
});
expect(service.deleteIndex).not.toHaveBeenCalled();

// The confirm modal renders the matching snippet for the action.
// The confirm modal renders the real snippets for the action.
const modalText = wrapper.find('[role="dialog"]').text();
expect(modalText).toContain(
'frosh-tools.tabs.elasticsearch.confirm.deleteIndex.title'
);
expect(modalText).toContain(
'frosh-tools.tabs.elasticsearch.confirm.deleteIndex.confirm'
);
expect(modalText).toContain('Delete index "shopware-product"?');
expect(modalText).toContain('Delete index');
});

it('does not run the action when the confirmation is cancelled', async () => {
Expand All @@ -98,17 +91,12 @@ describe('frosh-tools-tab-elasticsearch destructive actions', () => {
const wrapper = await createWrapper(service);
await flushPromises();

const notifySuccess = jest.spyOn(
wrapper.vm,
'createNotificationSuccess'
);

wrapper.vm.askFlushAll();
await wrapper.vm.runConfirmedAction();
await flushPromises();

expect(service.flushAll).toHaveBeenCalledTimes(1);
expect(notifySuccess).toHaveBeenCalled();
expect(notificationsOfVariant('success')).toHaveLength(1);
expect(wrapper.vm.confirmAction).toBeNull();
// Refreshed status + indices after the action.
expect(service.status).toHaveBeenCalledTimes(2);
Expand All @@ -121,13 +109,13 @@ describe('frosh-tools-tab-elasticsearch destructive actions', () => {
const wrapper = await createWrapper(service);
await flushPromises();

const notifyError = jest.spyOn(wrapper.vm, 'createNotificationError');

wrapper.vm.askReset();
await wrapper.vm.runConfirmedAction();

expect(service.reset).toHaveBeenCalledTimes(1);
expect(notifyError).toHaveBeenCalledWith({ message: 'boom' });
expect(
notificationsOfVariant('error').map((n) => n.message)
).toContain('boom');
expect(wrapper.vm.confirmAction).not.toBeNull();
expect(wrapper.vm.isConfirmingAction).toBe(false);
});
Expand Down
Loading
Loading