Skip to content

Commit

Permalink
Merge pull request #9 from heal-dev/wlo/test_config
Browse files Browse the repository at this point in the history
[GitHub action] Allow for test-config at the suite-level
  • Loading branch information
malomarrec authored Dec 3, 2024
2 parents b31abc6 + d31e074 commit 453f701
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 55 deletions.
51 changes: 42 additions & 9 deletions .github/workflows/test-action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
node-version-file: ".nvmrc"
cache: npm

- name: Download deps
Expand Down Expand Up @@ -51,28 +51,55 @@ jobs:
wait-for-results: "yes"
domain: "https://api-staging.heal.dev"
comment-on-pr: "yes"

- name: Run Action
id: my-action_new
uses: ./
with:
api-token: ${{ secrets.HEAL_API_TOKEN }}
suite: "testcitriggers/heal-core-functions"
suite: "wen/trigger"
test-config: |
{
"entrypoint": "https://www.wikipedia.org/",
"variables": {
"hello": "test level"
}
}
stories: |
[
{
"slug": "create-a-story-with-left-click",
"slug": "new-test",
"test-config": {
"entrypoint": "https://app-staging.heal.dev",
"variables": {
"blockName": "Test block"
}
"entrypoint": "https://www.ikea.com/fr/fr/",
"variables": {
"hello": "story level"
}
}
}
]
wait-for-results: "yes"
domain: "https://api-staging.heal.dev"
comment-on-pr: "yes"

- name: Run Action
id: my-action_new_with_test_config
uses: ./
with:
api-token: ${{ secrets.HEAL_API_TOKEN }}
suite: "wen/trigger"
test-config: |
{
"entrypoint": "https://www.wikipedia.org/",
"variables": {
"hello": "you"
}
}
stories: |
[
{
"slug": "new-test"
}
]
wait-for-results: "yes"
domain: "https://api-staging.heal.dev"
- name: Assert Result my-action_old
run: |
if [[ "${{ steps.my-action_old.outputs.execution-id }}" == "" ]]; then
Expand All @@ -85,3 +112,9 @@ jobs:
echo "Action failed to return an execution ID."
exit 1
fi
- name: Assert Result my-action_new_with_test_config
run: |
if [[ "${{ steps.my-action_new_with_test_config.outputs.execution-id }}" == "" ]]; then
echo "Action failed to return an execution ID."
exit 1
fi
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ project-slug-name/suite-slug-name (e.g., my-cool-project/end-to-end-tests).
uses: heal-dev/trigger@main
with:
api-token: ${{ secrets.HEAL_API_TOKEN }} # Required: Your Heal API token.
suite: "project-test/suite-test" # Required: The ID of the test suite.
suite: "project-test/suite-test" # Required: The ID of the test suite
test-config: | # Global test configuration
{
"entrypoint": "https://app-staging.heal.dev", # URL to override the default entry point.
"variables": # Variables to customize the test configuration.
{
"buttonName": "Test"
}
}
stories: | # Optional: JSON payload for the action.
[
{
Expand All @@ -33,7 +41,7 @@ project-slug-name/suite-slug-name (e.g., my-cool-project/end-to-end-tests).
"entrypoint": "https://app-staging.heal.dev", # URL to override the default entry point.
"variables": # Variables to customize the test configuration.
{
"buttonName": "Test"
"buttonName": "Test Story"
}
}
}
Expand All @@ -49,11 +57,20 @@ project-slug-name/suite-slug-name (e.g., my-cool-project/end-to-end-tests).
| ------------------ | -------- | --------------------------------------------------------------------------------------- |
| `api-token` | ✅ | Your Heal API token (you can create one [here](https://app.heal.dev/organisation/keys)) |
| `suite` | ✅ | The slug name of the test suite (e.g., project-slug-name/suite-slug-name). |
| `stories` | ❌ | Optional JSON payload to specify story slugs and override test configurations |
| `test-config` | ❌ | Optional JSON payload to specify global test configuration. |
| `stories` | ❌ | Optional JSON payload to specify story slugs and override global test configurations |
| `wait-for-results` | ❌ | Whether to wait for results (default: `yes`). |
| `domain` | ❌ | (default: `https://api.heal.dev`). |
| `comment-on-pr` | ❌ | Whether to comment test results on PR (default: `no`). |

### Test Configuration (test-config)

The test-config input allows you to customize test parameters, such as the entry point URL or specific variables. You can define it at two levels:

**Global Configuration (Suite Level)**: Applies to all stories in the suite unless overridden by a local configuration.

**Local Configuration (Story Level)**: Overrides the global configuration for specific stories.

### Using Suite ID (legacy)

Use this method if you already have the numeric ID of the test suite and optionally the ID of the specific story you want to run from Heal.dev.
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ inputs:
description: "Project slug name and Suite slug name, e.g. my-project/my-suite"
required: false
default: ""
test-config:
description: "Global configuration in JSON format"
required: false
stories:
description: "List of stories to run in JSON format"
required: false
Expand Down
71 changes: 49 additions & 22 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,25 +133,38 @@ function validatePayloadFormat(payload) {
});
}

function validateStoriesFormat(stories) {
if (!Array.isArray(stories)) {
function validateStoriesFormat(config) {
if (config['test-config']) {
const testConfig = config['test-config'];
if (testConfig.entrypoint && typeof testConfig.entrypoint !== 'string') {
throw new Error(`Invalid test-config: "entrypoint" must be a string if provided. Found ${typeof testConfig.entrypoint}.`);
}
if (testConfig.variables && typeof testConfig.variables !== 'object') {
throw new Error(`Invalid test-config: "variables" must be an object if provided. Found ${typeof testConfig.variables}.`);
}
}

if (config.stories && !Array.isArray(config.stories)) {
throw new Error('Invalid stories: "stories" must be an array.');
}

stories.forEach(story => {
if (typeof story.slug !== 'string') {
throw new Error(`Invalid story: "slug" must be a string. Found ${typeof story.slug}.`);
}
if (story['test-config']) {
const testConfig = story['test-config'];
if (testConfig.entrypoint && typeof testConfig.entrypoint !== 'string') {
throw new Error(`Invalid test-config: "entrypoint" must be a string if provided. Found ${typeof testConfig.entrypoint}.`);
if (config.stories) {
config.stories.forEach(story => {
if (typeof story.slug !== 'string') {
throw new Error(`Invalid story: "slug" must be a string. Found ${typeof story.slug}.`);
}
if (testConfig.variables && typeof testConfig.variables !== 'object') {
throw new Error(`Invalid test-config: "variables" must be an object if provided. Found ${typeof testConfig.variables}.`);
if (story['test-config']) {
const testConfig = story['test-config'];
if (testConfig.entrypoint && typeof testConfig.entrypoint !== 'string') {
throw new Error(`Invalid test-config: "entrypoint" must be a string if provided. Found ${typeof testConfig.entrypoint}.`);
}
if (testConfig.variables && typeof testConfig.variables !== 'object') {
throw new Error(`Invalid test-config: "variables" must be an object if provided. Found ${typeof testConfig.variables}.`);
}
}
}
});

});
}
}

function validateInput(inputType, input) {
Expand All @@ -175,6 +188,7 @@ async function run() {
const suite = core.getInput('suite');
const payload = core.getInput('payload');
const stories = core.getInput('stories');
const testConfig = core.getInput('test-config');

if (suiteId && suite) {
core.setFailed('Please provide either suite-id or suite, not both.');
Expand All @@ -185,8 +199,8 @@ async function run() {
return;
}

if (suiteId && stories) {
core.setFailed('When "suite-id" is provided, "stories" should come from "payload", not "stories".');
if (suiteId && (stories || testConfig)) {
core.setFailed('When "suite-id" is provided, "stories" should come from "payload", not "stories" or "test-config".');
return;
}

Expand All @@ -203,7 +217,8 @@ async function run() {

/**
* @type {{ stories: { id: number, entryHref: string, variables?: Record<string, string> }[]} ||
* { stories: { slug: string, "test-config"?: { entrypoint?: string, variables?: Record<string, string> } }[] }}
* { stories: { slug: string, "test-config"?: { entrypoint?: string, variables?: Record<string, string> } }[],
* "test-config"?: { entrypoint?: string, variables?: Record<string, string> } }}
*/
let validatedPayload;
try {
Expand All @@ -214,9 +229,17 @@ async function run() {
if (suiteId && inputPayload) {
validatedPayload = JSON.parse(inputPayload);
validateInput('payload', validatedPayload);
} else if (inputStories && suite) {
validatedPayload = { stories: JSON.parse(inputStories) };
validateInput('stories', validatedPayload.stories);
} else if (suite) {
validatedPayload = {};
if (inputStories) {
validatedPayload.stories = JSON.parse(inputStories);
}
if (testConfig) {
validatedPayload["test-config"] = JSON.parse(testConfig);
}
if (validatedPayload) {
validateInput('stories', validatedPayload, testConfig);
}
} else {
validatedPayload = suiteId ? {} : { stories: [] };
}
Expand Down Expand Up @@ -311,15 +334,19 @@ async function run() {
allPassed = false;
}
}
try {
await createTestSummary(report, report.link);
core.info('Posted test summary to summary section.');
} catch (error) {
core.warning(`Failed to post test summary: ${error.message}`);
}

// Post comment to PR if requested
if (commentOnPr === 'yes' || commentOnPr === 'true') {
try {
const comment = formatTestResults(report, report.link);
await createPRComment(githubToken, comment);
core.info('Posted test results to PR comment.');
await createTestSummary(report, report.link);
core.info('Posted test summary to summary section.');
} catch (error) {
core.warning(`Failed to post PR comment: ${error.message}`);
}
Expand Down
63 changes: 43 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,25 +127,38 @@ function validatePayloadFormat(payload) {
});
}

function validateStoriesFormat(stories) {
if (!Array.isArray(stories)) {
function validateStoriesFormat(config) {
if (config['test-config']) {
const testConfig = config['test-config'];
if (testConfig.entrypoint && typeof testConfig.entrypoint !== 'string') {
throw new Error(`Invalid test-config: "entrypoint" must be a string if provided. Found ${typeof testConfig.entrypoint}.`);
}
if (testConfig.variables && typeof testConfig.variables !== 'object') {
throw new Error(`Invalid test-config: "variables" must be an object if provided. Found ${typeof testConfig.variables}.`);
}
}

if (config.stories && !Array.isArray(config.stories)) {
throw new Error('Invalid stories: "stories" must be an array.');
}

stories.forEach(story => {
if (typeof story.slug !== 'string') {
throw new Error(`Invalid story: "slug" must be a string. Found ${typeof story.slug}.`);
}
if (story['test-config']) {
const testConfig = story['test-config'];
if (testConfig.entrypoint && typeof testConfig.entrypoint !== 'string') {
throw new Error(`Invalid test-config: "entrypoint" must be a string if provided. Found ${typeof testConfig.entrypoint}.`);
if (config.stories) {
config.stories.forEach(story => {
if (typeof story.slug !== 'string') {
throw new Error(`Invalid story: "slug" must be a string. Found ${typeof story.slug}.`);
}
if (testConfig.variables && typeof testConfig.variables !== 'object') {
throw new Error(`Invalid test-config: "variables" must be an object if provided. Found ${typeof testConfig.variables}.`);
if (story['test-config']) {
const testConfig = story['test-config'];
if (testConfig.entrypoint && typeof testConfig.entrypoint !== 'string') {
throw new Error(`Invalid test-config: "entrypoint" must be a string if provided. Found ${typeof testConfig.entrypoint}.`);
}
if (testConfig.variables && typeof testConfig.variables !== 'object') {
throw new Error(`Invalid test-config: "variables" must be an object if provided. Found ${typeof testConfig.variables}.`);
}
}
}
});

});
}
}

function validateInput(inputType, input) {
Expand All @@ -169,6 +182,7 @@ async function run() {
const suite = core.getInput('suite');
const payload = core.getInput('payload');
const stories = core.getInput('stories');
const testConfig = core.getInput('test-config');

if (suiteId && suite) {
core.setFailed('Please provide either suite-id or suite, not both.');
Expand All @@ -179,8 +193,8 @@ async function run() {
return;
}

if (suiteId && stories) {
core.setFailed('When "suite-id" is provided, "stories" should come from "payload", not "stories".');
if (suiteId && (stories || testConfig)) {
core.setFailed('When "suite-id" is provided, "stories" should come from "payload", not "stories" or "test-config".');
return;
}

Expand All @@ -197,7 +211,8 @@ async function run() {

/**
* @type {{ stories: { id: number, entryHref: string, variables?: Record<string, string> }[]} ||
* { stories: { slug: string, "test-config"?: { entrypoint?: string, variables?: Record<string, string> } }[] }}
* { stories: { slug: string, "test-config"?: { entrypoint?: string, variables?: Record<string, string> } }[],
* "test-config"?: { entrypoint?: string, variables?: Record<string, string> } }}
*/
let validatedPayload;
try {
Expand All @@ -208,9 +223,17 @@ async function run() {
if (suiteId && inputPayload) {
validatedPayload = JSON.parse(inputPayload);
validateInput('payload', validatedPayload);
} else if (inputStories && suite) {
validatedPayload = { stories: JSON.parse(inputStories) };
validateInput('stories', validatedPayload.stories);
} else if (suite) {
validatedPayload = {};
if (inputStories) {
validatedPayload.stories = JSON.parse(inputStories);
}
if (testConfig) {
validatedPayload["test-config"] = JSON.parse(testConfig);
}
if (validatedPayload) {
validateInput('stories', validatedPayload, testConfig);
}
} else {
validatedPayload = suiteId ? {} : { stories: [] };
}
Expand Down
Loading

0 comments on commit 453f701

Please sign in to comment.