Skip to content

Commit

Permalink
feat: add task to upload asset to Sauce for spec test job (#86)
Browse files Browse the repository at this point in the history
* feat: add task to upload assets to Sauce for spec test jobs

* docs: add example

* add more validation

* rename function

* add comments to explain why using spec basename as the key

* rename specAssets to specAssetsMap

* add comment for specAssetsMap

* upload multiple assets

* fix README

* use Variadic function

* update README

* fix format

* update integration test

* put the reading stream in a task

* use txt extension

* revert debug step

* kill all warnings

* reword

* refine comment

* remove data param

* make filename optional

* revert integration setup for fs operation

* update test

* eslint for Buffer is not required

* make the doc more natural to read

* refine examples
  • Loading branch information
tianfeng92 authored Oct 29, 2024
1 parent 06ad192 commit 3e7c032
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 22 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
node-version-file: ".nvmrc"
cache: "npm"

- name: Install Dependencies
run: npm ci
Expand Down
68 changes: 50 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ Sauce Labs. Your Sauce Labs Username and Access Key are available from your
Example `cypress.config.cjs`:

```javascript
const { defineConfig } = require('cypress');
const { defineConfig } = require("cypress");

module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
require('@saucelabs/cypress-plugin').default(on, config, {
region: 'us-west-1',
build: 'myBuild',
tags: ['example1'],
require("@saucelabs/cypress-plugin").default(on, config, {
region: "us-west-1",
build: "myBuild",
tags: ["example1"],
});
return config;
},
Expand All @@ -51,16 +51,16 @@ module.exports = defineConfig({
Example `cypress.config.mjs`:

```javascript
import { defineConfig } from 'cypress';
import reporter from '@saucelabs/cypress-plugin';
import { defineConfig } from "cypress";
import reporter from "@saucelabs/cypress-plugin";

export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
reporter.default(on, config, {
region: 'us-west-1',
build: 'myBuild',
tags: ['example1'],
region: "us-west-1",
build: "myBuild",
tags: ["example1"],
});
return config;
},
Expand All @@ -71,16 +71,16 @@ export default defineConfig({
Example `cypress.config.ts`:

```typescript
import { defineConfig } from 'cypress';
import Reporter, { Region } from '@saucelabs/cypress-plugin';
import { defineConfig } from "cypress";
import Reporter, { Region } from "@saucelabs/cypress-plugin";

export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
Reporter(on, config, {
region: Region.USWest1, // us-west-1 is the default
build: 'myBuild',
tags: ['example1'],
build: "myBuild",
tags: ["example1"],
});
return config;
},
Expand All @@ -96,10 +96,10 @@ Register the plugin in your project's `cypress/plugins/index.js`:
module.exports = (on, config) => {
// Other plugins you may already have.
// ...
require('@saucelabs/cypress-plugin').default(on, config, {
region: 'us-west-1',
build: 'myBuild',
tags: ['example1'],
require("@saucelabs/cypress-plugin").default(on, config, {
region: "us-west-1",
build: "myBuild",
tags: ["example1"],
});
return config;
};
Expand Down Expand Up @@ -134,6 +134,38 @@ Jobs reported to Sauce Labs:
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
```

## Upload Assets Task

This task allows you to upload assets (such as images or logs) to a specific Sauce Labs job associated with the test spec.

| Parameter | Type | Description |
| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `spec` | `string` | Path to the spec file being executed, typically provided by `__filename`. |
| `assets` | `Asset` \| `Asset[]` | Can be a single `Asset` object or an array of `Asset` objects to be uploaded to Sauce Labs. Each `Asset` should contain a `filename` and either a `path` or `data`. |
| `assets[].path` | `string` | **Required**. Path to the file on the local filesystem (e.g., `"pics/this-is-fine.png"`). |
| `assets[].filename` | `string` | **Optional**. The name of the file to upload, as it should appear in Sauce Labs (e.g., `"this-is-fine.png"`). If not provided, the file path basename is used by default. |

### Example Usage

```javascript
it("upload assets", () => {
// Single file upload.
cy.task("sauce:uploadAssets", {
spec: __filename,
assets: { path: "pics/this-is-fine.png" },
});

// Multiple files upload.
cy.task("sauce:uploadAssets", {
spec: __filename,
assets: [
{ path: "pics/this-is-fine.png" },
{ path: "test.txt", filename: "test.log" },
],
});
});
```

## Real-life Example

[tests/integration/](https://github.com/saucelabs/sauce-cypress-plugin/tree/main/tests/integration/) folder will present an integration example with [Cypress' Kitchensink](https://github.com/cypress-io/cypress-example-kitchensink/tree/master/cypress/e2e/2-advanced-examples) tests set.
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default ts.config(
languageOptions: {
globals: {
__dirname: true,
__filename: true,
console: true,
exports: true,
module: true,
Expand Down
54 changes: 53 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Reporter from "./reporter";
import Table from "cli-table3";
import chalk from "chalk";
import path from "node:path";
import fs from "node:fs";
import { Asset } from "@saucelabs/testcomposer";
import BeforeRunDetails = Cypress.BeforeRunDetails;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import PluginEvents = Cypress.PluginEvents;
Expand All @@ -20,6 +23,9 @@ export interface Options {
let reporterInstance: Reporter;
const reportedSpecs: { name: string; jobURL: string }[] = [];

// Maps spec names to their associated assets, cached for later upload.
let specToAssets: Map<string, Asset[]>;

const isAccountSet = function () {
return process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY;
};
Expand All @@ -40,7 +46,7 @@ const onAfterSpec = async function (
}

try {
const job = await reporterInstance.reportSpec(results);
const job = await reporterInstance.reportSpec(results, specToAssets);
if (!job?.id || !job?.url) {
return;
}
Expand Down Expand Up @@ -136,13 +142,59 @@ export async function afterRunTestReport(
return reportJSON;
}

/**
* Temporarily caches assets for the current spec test job.
* Assets collected by this function are stored and later uploaded in `onAfterSpec`.
**/
const cacheAssets = ({
spec,
...assets
}: {
spec: string;
assets: Asset[];
}): null => {
if (!spec) {
throw new Error("'spec' is required.");
}
const assetsArray = Object.values(assets).flat();
if (assetsArray.length === 0) {
throw new Error("'assets' is required.");
}
assetsArray.forEach((asset: Asset) => {
if (!asset.path) {
throw new Error("'path' is required.");
}
const resolvedPath = path.resolve(asset.path);
if (!fs.existsSync(resolvedPath)) {
throw new Error(`File not found at path '${resolvedPath}'.`);
}
asset.data = fs.createReadStream(resolvedPath);
if (!asset.filename) {
asset.filename = path.basename(asset.path);
}
});

// The spec name in the Cypress report uses the basename of the spec file.
// To ensure matching during upload, convert it to the basename here as well.
const specName = path.basename(spec);
const currAssets = specToAssets.get(specName) || [];
currAssets.push(...assetsArray);
specToAssets.set(specName, currAssets);

return null; // Cypress task requirement.
};

export default function (
on: PluginEvents,
config: PluginConfigOptions,
opts?: Options,
) {
reporterInstance = new Reporter(undefined, opts);
specToAssets = new Map();

on("task", {
"sauce:uploadAssets": cacheAssets,
});
on("before:run", onBeforeRun);
on("after:run", onAfterRun);
on("after:spec", onAfterSpec);
Expand Down
3 changes: 2 additions & 1 deletion src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default class Reporter {
}

// Reports a spec as a Job on Sauce.
async reportSpec(result: RunResult) {
async reportSpec(result: RunResult, specToAssets: Map<string, Asset[]>) {
if (!this.testComposer) {
return;
}
Expand All @@ -147,6 +147,7 @@ export default class Reporter {

const report = await this.createSauceTestReport([result]);
const assets = await this.collectAssets([result], report);
assets.push(...(specToAssets.get(result.spec.name) || []));
await this.uploadAssets(job.id, assets);

return job;
Expand Down
17 changes: 17 additions & 0 deletions tests/integration/cypress/e2e/1-getting-started/todo.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,21 @@ describe("example to-do app", () => {
cy.get(".todo-list li").first().should("have.text", "Pay electric bill");
cy.get(".todo-list li").last().should("have.text", "Walk the dog");
});

it("uploads assets", () => {
// Single file upload.
cy.task("sauce:uploadAssets", {
spec: __filename,
assets: { filename: "test1.log", path: "test.txt" },
});

// Multiple files upload.
cy.task("sauce:uploadAssets", {
spec: __filename,
assets: [
{ filename: "test2.log", path: "test.txt" },
{ path: "test.txt" },
],
});
});
});
1 change: 1 addition & 0 deletions tests/integration/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is the log file, which will be uploaded during the Cypress task test.

0 comments on commit 3e7c032

Please sign in to comment.