Movatterモバイル変換


[0]ホーム

URL:


Another core feature of Codecov is our processing and merging of coverage reports. That means we take in any number of reports and aggregate them together, so you can see your coverage data in one view. Let’s see what that looks like.

Write a frontend

Pull the latest frommain and create a new branchstep4

git checkout maingit pullgit checkout -b 'step4'

Next, we are going to build out a frontend for our calculator app. We are going to be adding aweb directory with a few files. Create the files by running these commands from the root directory:

mkdir -p web/static/cssmkdir -p web/static/jstouch web/.babelrctouch web/index.htmltouch web/package.jsontouch web/server.jstouch web/static/css/calculator.csstouch web/static/js/calculator.jstouch web/static/js/calculatorView.js

Copy and paste the contents below into each file:

{  "env": {    "test": {      "plugins": ["@babel/plugin-transform-modules-commonjs"]    }  }}
<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>Codecov Server-side Calculator App</title>    <link rel="stylesheet" href="/static/css/calculator.css"></head><body>    <div class="calculator">        <div class="output">            <div data-previous-operand class="previous-operand">123 +</div>            <div data-current-operand class="current-operand">456</div>        </div>        <button class="span-3 disabled"></button>        <button data-all-clear>C</button>        <button data-number>7</button>        <button data-number>8</button>        <button data-number>9</button>        <button data-operation>÷</button>        <button data-number>4</button>        <button data-number>5</button>        <button data-number>6</button>        <button data-operation>*</button>        <button data-number>1</button>        <button data-number>2</button>        <button data-number>3</button>        <button data-operation>-</button>        <button data-number>.</button>        <button data-number>0</button>        <button data-equals>=</button>        <button data-operation>+</button>    </div>    <script type="module" src="/static/js/calculatorView.js"></script></body></html>
{  "name": "web",  "version": "1.0.0",  "description": "",  "scripts": {    "test": "jest --collectCoverage",    "build": "babel --plugins @babel/plugin-transform-modules-commonjs script.js"  },  "keywords": [],  "author": "",  "license": "MIT",  "dependencies": {    "axios": "^0.25.0",    "express": "^4.17.2"  },  "devDependencies": {    "@babel/plugin-transform-modules-commonjs": "^7.16.8",    "jest": "^27.4.7"  }}
const axios = require('axios')const express = require("express");const path = require("path");const app = express();const backendHost = 'http://localhost';const backendPort = '8080';app.use(express.json());app.use("/static", express.static(path.resolve(__dirname, "static")));app.post("/api/:operation", (req, res) => {  axios.post(    backendHost + ':' +  backendPort + '/api/' + req.params['operation'],    req.body  ).then(response => {    res.json(response.data);  }).catch(error => {    console.log("Error: " + error);  });});app.get("/", (req, res) => {  res.sendFile(path.resolve("index.html"));});app.listen(process.env.PORT || 3000, () => console.log("Server running..."));

Inweb/static, copy and paste the contents below into each file:

*, *::before, *::after {  box-sizing: border-box;}body {  margin: 0;  padding: 0;  background-color: #ccc;}.calculator {  align-content: center;  display: grid;  grid-template-columns: repeat(4, 100px);  grid-template-rows: minmax(120px, auto) repeat(5, 100px);  justify-content: center;  min-height: 100vh;}.calculator > button {  background-color: rbga(255, 255, 255, 0.75);  border: 1px solid #FFFFFF;  cursor: pointer;  font-size: 32px;  outline: none;}.calculator > button:hover {  background-color: #aaaaaa;}.span-3 {  grid-column: span 3;}.disabled:hover {  background-color: rbga(255, 255, 255, 0.75);  cursor: not-allowed;}.output{  align-items: flex-end;  background-color: rgba(0, 0, 0, 0.75);  display: flex;  flex-direction: column;  grid-column: 1 / -1;  justify-content: space-around;  padding: 10px;  word-break: break-all;  word-wrap: break-word;}.output .previous-operand{  color: rgba(255,255, 255, 0.75);}.output .current-operand{  color: white;  font-size: 42px;}
export class Calculator {  constructor(previousOperandTextElement, currentOperandTextElement) {    this.previousOperandTextElement = previousOperandTextElement    this.currentOperandTextElement = currentOperandTextElement    this.clear()  }  clear() {    this.currentOperand = ''    this.previousOperand = ''    this.operation = undefined    this.validOperand = true  }  delete() {    this.currentOperand = this.currentOperand.toString().slice(0, -1)  }  appendNumber(number) {    if (!this.validOperand) return    if (number === '.' && this.currentOperand.includes('.')) return    this.currentOperand = this.currentOperand.toString() + number.toString()  }  chooseOperation(operation) {    if (!this.validOperand) return    if (this.currentOperand === '') return    if (this.previousOperand !== '') {      this.compute()    }    this.operation = operation    this.previousOperand = this.currentOperand    this.currentOperand = ''  }  compute() {    if (!this.validOperand) {      return;    }    const prev = parseFloat(this.previousOperand)    const current = parseFloat(this.currentOperand)    if (isNaN(prev) || isNaN(current)) return    let operation    switch (this.operation) {      case '+':        operation = 'add'        break      case '-':        operation = 'subtract'        break      case '*':        operation = 'multiply'        break      case '÷':        operation = 'divide'        break      default:        return    }    this.callApi(operation)  }  async callApi(operation) {    const response = await fetch("/api/" + operation, {      method: 'POST',      headers: {        'Accept': 'application/json',        'Content-Type': 'application/json'      },      body: JSON.stringify({        x: this.previousOperand,        y: this.currentOperand      })    })    if (!response.ok) {      throw new Error("Error: " + response.status);    }    this.currentOperand = await response.json();    this.operation = undefined;    this.previousOperand = '';    this.updateDisplay();  }  getDisplayNumber(number) {    const stringNumber = number.toString()    const integerDigits = parseFloat(stringNumber.split('.')[0])    const decimalDigits = stringNumber.split('.')[1]    let integerDisplay    if (isNaN(integerDigits)) {      integerDisplay = stringNumber;      if (stringNumber.length > 0) {        this.validOperand = false;      }    } else {      integerDisplay = integerDigits.toLocaleString('en', { maximumFractionDigits: 0 })    }    if (decimalDigits != null) {      return `${integerDisplay}.${decimalDigits}`    } else {      return integerDisplay    }  }  updateDisplay() {    this.currentOperandTextElement.innerText =      this.getDisplayNumber(this.currentOperand)    if (this.operation != null) {      this.previousOperandTextElement.innerText =        `${this.getDisplayNumber(this.previousOperand)} ${this.operation}`    } else {      this.previousOperandTextElement.innerText = ''    }  }}
import { Calculator } from './calculator.js';const numberButtons = document.querySelectorAll('[data-number]')const operationButtons = document.querySelectorAll('[data-operation]')const equalsButton = document.querySelector('[data-equals]')const allClearButton = document.querySelector('[data-all-clear]')const previousOperandTextElement = document.querySelector('[data-previous-operand]')const currentOperandTextElement = document.querySelector('[data-current-operand]')const calculator = new Calculator(previousOperandTextElement, currentOperandTextElement)numberButtons.forEach(button => {  button.addEventListener('click', () => {    calculator.appendNumber(button.innerText)    calculator.updateDisplay()  })})operationButtons.forEach(button => {  button.addEventListener('click', () => {    calculator.chooseOperation(button.innerText)    calculator.updateDisplay()  })})equalsButton.addEventListener('click', button => {  calculator.compute()})allClearButton.addEventListener('click', button => {  calculator.clear()  calculator.updateDisplay()})document.addEventListener('keydown', function (event) {  let patternForNumbers = /[0-9]/g;  let patternForOperators = /[+\-*\/]/g  if (event.key.match(patternForNumbers)) {    event.preventDefault();    calculator.appendNumber(event.key)    calculator.updateDisplay()  }  if (event.key === '.') {    event.preventDefault();    calculator.appendNumber(event.key)    calculator.updateDisplay()  }  if (event.key.match(patternForOperators)) {    event.preventDefault();    calculator.chooseOperation(event.key)    calculator.updateDisplay()  }  if (event.key === 'Enter' || event.key === '=') {    event.preventDefault();    calculator.compute()  }});

We can test out our frontend by running

cd webnpm installnode server.js

Be sure to run the backend server as well - in a different terminal:

cd apiflask run --port 8080 --host=0.0.0.0

Go tohttp://localhost:3000 to view the calculator

Feel free to play around with the interface.

Writing frontend tests

Let’s add tests to ourcalculator.js file

touch web/static/js/calculator.test.js
const calc = require('./calculator');describe('Calculator test suite', () => {  test('calculator clears', () => {    const calculator = new calc.Calculator();    calculator.clear();    expect(calculator.currentOperand).toBe('');    expect(calculator.previousOperand).toBe('');    expect(calculator.operation).toBe(undefined);    expect(calculator.validOperand).toBe(true);  })  test('calculator can input numbers', () => {    const calculator = new calc.Calculator();    calculator.appendNumber(1);    expect(calculator.currentOperand).toBe('1');    calculator.appendNumber(2);    expect(calculator.currentOperand).toBe('12');  })  test('calculator can delete', () => {    const calculator = new calc.Calculator();    calculator.appendNumber(1);    expect(calculator.currentOperand).toBe('1');    calculator.appendNumber(2);    expect(calculator.currentOperand).toBe('12');    calculator.delete();    expect(calculator.currentOperand).toBe('1');  })})

Run the tests

We can run these tests from theweb directory with the following command

npm run test

and we should see the following output

PASS  static/js/calculator.test.js  Calculator test suite    ✓ calculator clears (3 ms)    ✓ calculator can input numbers    ✓ calculator can deletes---------------|---------|----------|---------|---------|-------------------File           | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s---------------|---------|----------|---------|---------|-------------------All files      |   18.03 |     9.09 |   44.44 |   19.64 | calculator.js |   18.03 |     9.09 |   44.44 |   19.64 | 26-113---------------|---------|----------|---------|---------|-------------------Test Suites: 1 passed, 1 totalTests:       3 passed, 3 totalSnapshots:   0 totalTime:        0.268 s

Add a frontend CI workflow

Let’s add a workflow to run our frontend tests. This workflow does a similar set of steps as our API workflow.

GitLab CI

Go back to the.gitlab-ci.yml file at the root of your project, add this below the code we addedpreviously forapi

frontend:  image: node:latest  script:    - cd web && npm install    - npm run test    - curl -Os https://uploader.codecov.io/latest/linux/codecov    - chmod +x codecov    - ./codecov -t $CODECOV_TOKEN

If we commit this code,

cd ..  # back to the root directorygit add .git commit -m 'step4: add frontend and tests'git push origin step4

and open a merge request, we see that our code coverage has greatly decreased! With the addition of the new, but poorly tested frontend, our status checks fail.

We also see that in the Codecov comment.

We see this type of situation happen all the time. For example, some projects in a monorepository might be starting code coverage for the first time, while other parts have been using Codecov for months or years. Adding code coverage like this can greatly decrease the percentage for the whole project.

However, what you may not have noticed is that Codecov automatically merges coverage reports together, regardless of the language or CI workflow. This happens under the hood and requires no additional configuration from you.

In the next section, we'll show you how to useCodecov Flags to help group coverage reports and to set different coverage standards for different parts of your project.

Updated 11 months ago



[8]ページ先頭

©2009-2025 Movatter.jp