Skip to content
Merged
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
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ module.exports = {
'packages/plugins/**/built/*',
'packages/tests/src/_jest/**/*',
'packages/tests/src/_playwright/**/*',
'packages/tests/src/e2e/**/*',
],
rules: {
'import/no-extraneous-dependencies': 'off',
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
DD_ENV: ci

e2e:
timeout-minutes: 10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an explanation for this increase?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The e2e CI/CD job is very flaky. It can take between 5-30 minutes. It seems primarily because it can take a while to install playwright if it isn't cached.

timeout-minutes: 30

name: End to End
runs-on: ubuntu-latest
Expand Down
7 changes: 6 additions & 1 deletion packages/plugins/apps/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ describe('Apps Plugin - getPlugins', () => {
await plugin.asyncTrueEnd?.();

expect(assets.collectAssets).toHaveBeenCalledWith(['dist/**/*'], buildRoot);
expect(archive.createArchive).toHaveBeenCalledWith(mockedAssets);
expect(archive.createArchive).toHaveBeenCalledWith([
{
absolutePath: '/project/dist/index.js',
relativePath: path.join('frontend', 'dist/index.js'),
},
]);
expect(uploader.uploadArchive).toHaveBeenCalledWith(
expect.objectContaining({ archivePath: '/tmp/dd-apps-123/datadog-apps-assets.zip' }),
{
Expand Down
9 changes: 8 additions & 1 deletion packages/plugins/apps/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import chalk from 'chalk';
import path from 'path';

import { createArchive } from './archive';
import type { Asset } from './assets';
import { collectAssets } from './assets';
import { CONFIG_KEY, PLUGIN_NAME } from './constants';
import { resolveIdentifier } from './identifier';
Expand Down Expand Up @@ -65,8 +66,14 @@ Either:
return;
}

// Prefix all assets into the frontend/ subdirectory.
const allAssets: Asset[] = assets.map((asset) => ({
...asset,
relativePath: path.join('frontend', asset.relativePath),
}));

const archiveTimer = log.time('archive assets');
const archive = await createArchive(assets);
const archive = await createArchive(allAssets);
archiveTimer.end();
// Store variable for later disposal of directory.
archiveDir = path.dirname(archive.archivePath);
Expand Down
157 changes: 157 additions & 0 deletions packages/tests/src/e2e/appsPlugin/appsPlugin.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import { existsSync, outputJsonSync, readJsonSync } from '@dd/core/helpers/fs';
import { verifyProjectBuild } from '@dd/tests/_playwright/helpers/buildProject';
import type { TestOptions } from '@dd/tests/_playwright/testParams';
import { test } from '@dd/tests/_playwright/testParams';
import { defaultConfig } from '@dd/tools/plugins';
import type { Page } from '@playwright/test';
import JSZip from 'jszip';
import nock from 'nock';
import os from 'os';
import path from 'path';

// Have a similar experience to Jest.
const { expect, beforeAll, describe } = test;

const APP_IDENTIFIER = 'e2e-test-app-id';
const APP_NAME = 'e2e-test-app';
const CAPTURE_DIR = path.join(os.tmpdir(), 'dd-e2e-apps-plugin');

// Mock the apps upload endpoint and write captured request to a file.
// We write to a file because Playwright workers are separate processes —
// only the worker that actually builds captures the nock request.
nock('https://api.datadoghq.com')
.post(new RegExp(`/api/unstable/app-builder-code/apps/.*/upload`))
.reply(function handleUploadMock(uri, body) {
const captured = {
path: uri,
headers: this.req.headers as Record<string, string>,
body: typeof body === 'string' ? body : JSON.stringify(body),
};
// Write to a known location so all workers can read it.
outputJsonSync(path.join(CAPTURE_DIR, 'upload-capture.json'), captured);
return [
200,
{
version_id: 'v-test-123',
application_id: 'app-test-123',
app_builder_id: 'builder-test-123',
},
];
})
.persist();

const userFlow = async (url: string, page: Page, bundler: TestOptions['bundler']) => {
// Navigate to our page.
await page.goto(`${url}/index.html?context_bundler=${bundler}`);
await page.waitForSelector('body');
};

// Read captured upload request from the shared temp file.
const readUploadCapture = () => {
const capturePath = path.join(CAPTURE_DIR, 'upload-capture.json');
if (!existsSync(capturePath)) {
return null;
}
return readJsonSync(capturePath) as {
path: string;
headers: Record<string, string>;
body: string;
};
};

describe('Apps Plugin', () => {
// Build our fixture project with the apps plugin enabled and upload active.
beforeAll(async ({ publicDir, bundlers, suiteName }) => {
const source = path.resolve(__dirname, 'project');
const destination = path.resolve(publicDir, suiteName);
await verifyProjectBuild(source, destination, bundlers, {
...defaultConfig,
apps: {
enable: true,
dryRun: false,
identifier: APP_IDENTIFIER,
name: APP_NAME,
},
});
});

test('Should build and load the page without errors', async ({
page,
bundler,
browserName,
suiteName,
devServerUrl,
}) => {
const errors: string[] = [];
const testBaseUrl = `${devServerUrl}/${suiteName}`;

// Listen for errors on the page.
page.on('pageerror', (error) => errors.push(error.message));
page.on('response', async (response) => {
if (!response.ok()) {
const url = response.request().url();
const prefix = `[${bundler} ${browserName} ${response.status()}]`;
errors.push(`${prefix} ${url}`);
}
});

// Verify that we do log the expected things.
const logs: string[] = [];
page.on('console', async (msg) => {
if (msg.type() !== 'log') {
return;
}
for (const arg of msg.args()) {
// eslint-disable-next-line no-await-in-loop
logs.push(await arg.jsonValue());
}
});

// It should load the correct bundler file.
const bundleRequest = page.waitForResponse(`${testBaseUrl}/dist/${bundler}.js`);
await userFlow(testBaseUrl, page, bundler);
expect((await bundleRequest).ok()).toBe(true);

expect(logs).toEqual([`Hello from apps plugin, ${bundler}!`]);
expect(errors).toHaveLength(0);
});

test('Should have uploaded assets to the apps intake', async () => {
// Read captured upload from the shared file written by the building worker.
const uploadRequest = readUploadCapture();

// The upload happens during the build phase in beforeAll.
expect(uploadRequest).not.toBeNull();

// Verify the upload URL contains the app identifier.
expect(uploadRequest!.path).toContain(
`/api/unstable/app-builder-code/apps/${APP_IDENTIFIER}/upload`,
);

// Verify the origin headers are set.
expect(uploadRequest!.headers['dd-evp-origin']).toMatch(/-build-plugin_apps$/);
expect(uploadRequest!.headers['dd-evp-origin-version']).toBeDefined();

// The body is hex-encoded multipart form data. Decode it to verify contents.
const decodedBody = Buffer.from(uploadRequest!.body, 'hex').toString('utf-8');
expect(decodedBody).toContain(APP_NAME);
expect(decodedBody).toContain('datadog-apps-assets.zip');

// Extract the zip from the multipart body and verify all files are under frontend/.
const bodyBuffer = Buffer.from(uploadRequest!.body, 'hex');
const zipMagic = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
const zipStart = bodyBuffer.indexOf(zipMagic);
expect(zipStart).toBeGreaterThanOrEqual(0);

const zip = await JSZip.loadAsync(bodyBuffer.subarray(zipStart));
const filePaths = Object.keys(zip.files);
expect(filePaths.length).toBeGreaterThan(0);
for (const filePath of filePaths) {
expect(filePath).toMatch(/^frontend\//);
}
});
});
18 changes: 18 additions & 0 deletions packages/tests/src/e2e/appsPlugin/project/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" sizes="21x21" href="data:image/svg+xml,">
<title>Apps Plugin Test</title>
</head>

<body>
<h1>Welcome to the {{bundler}} Apps Plugin Test</h1>
<p>This page verifies the build output works with the apps plugin enabled.</p>

<script src="./dist/{{bundler}}.js"></script>
</body>

</html>
5 changes: 5 additions & 0 deletions packages/tests/src/e2e/appsPlugin/project/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

console.log('Hello from apps plugin, {{bundler}}!');
Loading