Skip to content

Commit

Permalink
feat(validation): UI for interacting with /api/validate endpoint (#5)
Browse files Browse the repository at this point in the history
* - Added CSS file rules for editorconfig (2-indent)
- Made background a little lighter

* Added main content container to root layout

* Initial UI for interacting with the validate API endpoint

* Added htmlFor to loading textarea to fix build

* Turn off the 'react/display-name' linting rule to allow for loading components via a .Loading attribute

* Fix bottom padding of option label

* Added empty default string values to additional classname parameters to avoid undefined classnames

* Install Cypress

* Initial workflow for E2E testing for the preview branch

* Specify baseUrl via config instead of env var

* - Update setup-go action version 4 -> 5
- Use cache in initial dependency installation job
- Don't install in final e2e testing stage
- Add install-deps as a dependency of the test-preview stage

* - Remove lookup-only from initial dep cache
- Add Cypress binary to cache

* Run E2E test against per-deployment URLs (allows for concurrent/cross-branch execution of e2e tests)

* Fix saving preview URL to GitHub output

* Add Cypress component config

* Initial component tests

* Add component tests to CI

* Don't deploy unless component tests pass

* Add required testId to TextArea component

* E2E test

* - Use beforeEach in component test to enforce DRY
- Change wording of "show excluded" to "show ignored"
- Added data-cy tags to options area and submit button

* - Added new testing commands
- Updated docs for testing
  • Loading branch information
DiggidyDev authored Mar 15, 2024
1 parent 18ab30a commit 0116624
Show file tree
Hide file tree
Showing 20 changed files with 2,297 additions and 174 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false

[*.yaml]
[*.{yaml,css,config.ts}]
indent_size = 2
5 changes: 4 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"rules": {
"react/display-name": "off"
}
}
86 changes: 83 additions & 3 deletions .github/workflows/vercel-preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,55 @@ on:
- main

jobs:
install-deps:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Cache dependencies
id: cache
uses: actions/cache@v4
with:
path: |
node_modules
~/.npm
~/.cache/Cypress
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

- if: steps.cache.outputs.cache-hit != 'true'
name: Install dependencies
run: npm ci

- if: steps.cache.outputs.cache-hit == 'true'
name: Link cached dependencies
run: npm install

test-components:
needs: install-deps
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Restore dependency cache
uses: actions/cache@v4
with:
path: |
node_modules
~/.npm
~/.cache/Cypress
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

- name: Link cached dependencies
run: npm install

- name: Run component tests
uses: cypress-io/github-action@v6
with:
install: false
component: true

test-api:
runs-on: ubuntu-latest
steps:
Expand All @@ -20,7 +69,7 @@ jobs:
api/validate
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: "1.18"

Expand All @@ -35,7 +84,9 @@ jobs:
go test -v
deploy-preview:
needs: test-api
needs: [test-api, test-components]
outputs:
preview-url: ${{ steps.set-preview-url.outputs.preview-url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -46,4 +97,33 @@ jobs:
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}

- name: Deploy directly to Vercel
run: vercel deploy --token=${{ secrets.VERCEL_TOKEN }}
run: vercel deploy --token=${{ secrets.VERCEL_TOKEN }} > preview-url.txt

- name: Set Preview URL
id: set-preview-url
run: echo "preview-url=$(cat preview-url.txt)" >> "$GITHUB_OUTPUT"

test-preview:
needs: [deploy-preview, install-deps]
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Restore dependency cache
uses: actions/cache@v4
with:
path: |
node_modules
~/.npm
~/.cache/Cypress
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

- name: Link cached dependencies
run: npm install

- name: Run E2E tests
uses: cypress-io/github-action@v6
with:
config: baseUrl=${{ needs.deploy-preview.outputs.preview-url }}
install: false
82 changes: 75 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,102 @@

- [About](#about)
- [Feature list](#feature-list)
- [Local Development](#local-development)
- [Testing](#testing)
- [Adding new tests](#adding-new-tests)
- [API Endpoints](#api-endpoints)
- [`/api/validate`](#apivalidate)

# About
## About

A tool to verify your `.dockerignore` file configuration.

## Features
### Features

- [ ] Visual interface
- [x] Visual interface
- [ ] Syntax guide for writing a well-formed `.dockerignore` file
- [ ] Input your `.dockerignore` configuration to validate its syntax
- [x] Input your `.dockerignore` configuration to validate its syntax
- [ ] Highlight any syntax errors (+ duplicate lines)
- [ ] Input a project/directory structure to match using the `.dockerignore`
- [x] Input a project/directory structure to match using the `.dockerignore`
- [ ] Display all files which will match against the config file, and those that won't
- [ ] Input a GitHub URL (public for now, maybe auth in future for private repos?)
- [ ] Automagically run the `.dockerignore` against the repo's structure
- [x] API
- [x] Allow programmatic `POST`s to a `/api/validate` endpoint
- [ ] Response includes information about the validity of the `.dockerignore`

# API Endpoints
## Local Development

As far as prerequisites go, you will need to have the [Vercel CLI installed](https://vercel.com/docs/cli#installing-vercel-cli).

To run this project locally, clone it onto your local machine.

```bash
git clone git@github.com:DiggidyDev/dockerignore-validator.git
```

You can start a local development server with:

```bash
vercel dev
```

### Testing

To open Cypress for local testing:

```bash
npm run test
```

To run all e2e tests headlessly:

```bash
npm run test:e2e
```

###### Ensure your development server is running for the e2e tests to run

To run all component tests headlessly:

```bash
npm run test:cmp
```

###### No development server is required for these tests to run

### Adding new tests

If you wish to add a new e2e test, it should live under:

```
cypress/
└── e2e/
└── TestName.cy.ts
```

##### Tree diagram generated courtesy of [Nathan Friend's Tree tool](https://tree.nathanfriend.io/)

If you wish to add a new component test, it should live under:

```
src/
└── app/
└── components/
└── ComponentName/
├── ComponentName.cy.tsx
└── ComponentName.tsx
```

##### Tree diagram generated courtesy of [Nathan Friend's Tree tool](https://tree.nathanfriend.io/)

## API Endpoints

When interacting with the API, no validation is required as of now.

It's important to note that each **Response example** is what would be returned from the endpoint as though the **Request Body** was provided.

## `/api/validate`
### `/api/validate`

- **Description**: Validates filepaths against a .dockerignore configuration.
- **Method**: `POST`
Expand Down
17 changes: 17 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig } from "cypress";

export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
setupNodeEvents(on, config) {
// implement node event listeners here
},
},

component: {
devServer: {
framework: "next",
bundler: "webpack",
},
},
});
99 changes: 99 additions & 0 deletions cypress/e2e/validate.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
describe("Homepage", () => {
beforeEach(() => {
cy.visit("/");
});

it("should have all inputs present and in the correct initial state", () => {
// Test inputs are present and valid
cy.get("[data-cy=dockerignore-input] > textarea")
.should("exist")
.and("be.enabled");
cy.get("[data-cy=dockerignore-input] > label")
.should("exist")
.and("have.text", ".dockerignore");

cy.get("[data-cy=files-input] > textarea")
.should("exist")
.and("be.enabled");
cy.get("[data-cy=files-input] > label")
.should("exist")
.and("have.text", "Files");

cy.get("[data-cy=options] > input[name=showIgnored]")
.should("exist")
.and("be.checked");

cy.get("[data-cy=validate-button]").should("exist").and("be.enabled");

// Test output is present and in the correct state
cy.get("[data-cy=result-output] > textarea")
.should("exist")
.and("be.disabled");
cy.get("[data-cy=result-output] > label")
.should("exist")
.and("have.text", "Ignored Files");
});

it("should allow form submission with correct ", () => {
const dockerignore = "node_modules";
const files = "node_modules\npackage.json";

// Output text with "Show ignored files" checked
const expectedIgnoredOutput = "node_modules";
// Output text with "Show ignored files" unchecked
const expectedCopyFilesOutput = "package.json";
// API Response
const expectedRes = [true, false];

// Ensure the correct data is being sent and received
cy.intercept("POST", "/api/validate", (req) => {
expect(JSON.parse(req.body)).to.deep.equal({
dockerignore: ["node_modules"],
files: ["node_modules", "package.json"],
});
req.continue((res) => {
expect(res.statusCode).to.equal(200);
expect(res.body).to.deep.equal(expectedRes);
});
}).as("validate");

cy.get("[data-cy=dockerignore-input] > textarea").type(dockerignore);
cy.get("[data-cy=files-input] > textarea").type(files);

// Ensure the output is empty and the loading state is not present
cy.get("[data-cy=result-output] > textarea").should("be.empty");
cy.get("[data-cy=textarea-loading]").should("not.exist");

// Once clicked, the result should be replaced with a loading state
cy.get("[data-cy=validate-button]").click();
cy.get("[data-cy=result-output]").should("not.exist");
cy.get("[data-cy=textarea-loading]").should("exist");
cy.get("[data-cy=textarea-loading] > label").should(
"have.text",
"Matching patterns..."
);

cy.wait("@validate");

// Loading state should be removed and the result should be present
cy.get("[data-cy=textarea-loading]").should("not.exist");
cy.get("[data-cy=result-output]").should("exist");

// Ensure the result has the correct output for both checked
// and unchecked states of "Show ignored files"
cy.get("[data-cy=result-output] > textarea").should(
"have.text",
expectedIgnoredOutput
);

cy.get("[data-cy=options] > input[name=showIgnored]").uncheck();
cy.get("[data-cy=result-output] > label").should(
"have.text",
"Files to Copy"
);
cy.get("[data-cy=result-output] > textarea").should(
"have.text",
expectedCopyFilesOutput
);
});
});
37 changes: 37 additions & 0 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
Loading

0 comments on commit 0116624

Please sign in to comment.