mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-15 16:19:30 -07:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4672d12d7 | ||
|
|
04ed3e0a52 | ||
|
|
58a337efbf | ||
|
|
249cbb93e6 | ||
|
|
888b35ea73 | ||
|
|
2a9e5b370e | ||
|
|
87d4155d71 |
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@@ -63,7 +63,17 @@
|
||||
"env": {
|
||||
"DIST": "1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Test",
|
||||
"program": "${workspaceFolder}/tests/index.js",
|
||||
"request": "launch",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"type": "node",
|
||||
"outputCapture": "std"
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
|
||||
2
dist/index.html
vendored
2
dist/index.html
vendored
File diff suppressed because one or more lines are too long
2
dist/resources/ws.min.js
vendored
2
dist/resources/ws.min.js
vendored
File diff suppressed because one or more lines are too long
12
index.js
12
index.js
@@ -31,17 +31,17 @@ const index = (req, res) => {
|
||||
};
|
||||
|
||||
// debugging
|
||||
if (process.env?.DIST !== '1') {
|
||||
// debugging
|
||||
app.get('/index.html', index);
|
||||
app.get('/', index);
|
||||
app.get('*', express.static(path.join(__dirname, './server')));
|
||||
} else {
|
||||
if (process.env?.DIST === '1') {
|
||||
// distribution
|
||||
app.use('/images', express.static(path.join(__dirname, './server/images')));
|
||||
app.use('/fonts', express.static(path.join(__dirname, './server/fonts')));
|
||||
app.use('/scripts', express.static(path.join(__dirname, './server/scripts')));
|
||||
app.use('/', express.static(path.join(__dirname, './dist')));
|
||||
} else {
|
||||
// debugging
|
||||
app.get('/index.html', index);
|
||||
app.get('/', index);
|
||||
app.get('*', express.static(path.join(__dirname, './server')));
|
||||
}
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
|
||||
8447
package-lock.json
generated
8447
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "5.9.7",
|
||||
"version": "5.9.8",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build:css": "sass ./server/styles/scss/style.scss ./server/styles/compiled.css",
|
||||
"lint": "eslint ./server/scripts/**",
|
||||
"lint:fix": "eslint --fix ./server/scripts/**"
|
||||
"lint": "eslint ./server/scripts/**/*.mjs",
|
||||
"lint:fix": "eslint --fix ./server/scripts/**/*.mjs"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,8 +25,8 @@
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-sonarjs": "^0.17.0",
|
||||
"eslint-plugin-unicorn": "^45.0.2",
|
||||
"eslint-plugin-sonarjs": "^0.19.0",
|
||||
"eslint-plugin-unicorn": "^46.0.0",
|
||||
"express": "^4.17.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
|
||||
@@ -30,8 +30,9 @@ class CurrentWeather extends WeatherDisplay {
|
||||
const filteredStations = weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
|
||||
|
||||
// Load the observations
|
||||
let observations; let
|
||||
station;
|
||||
let observations;
|
||||
let station;
|
||||
|
||||
// station number counter
|
||||
let stationNum = 0;
|
||||
while (!observations && stationNum < filteredStations.length) {
|
||||
@@ -73,7 +74,7 @@ class CurrentWeather extends WeatherDisplay {
|
||||
}
|
||||
|
||||
// we only get here if there was no error above
|
||||
this.data = { ...observations, station };
|
||||
this.data = parseData({ ...observations, station });
|
||||
this.getDataCallback();
|
||||
|
||||
// stop here if we're disabled
|
||||
@@ -84,89 +85,37 @@ class CurrentWeather extends WeatherDisplay {
|
||||
this.setStatus(STATUS.loaded);
|
||||
}
|
||||
|
||||
// format the data for use outside this function
|
||||
parseData() {
|
||||
if (!this.data) return false;
|
||||
const data = {};
|
||||
const observations = this.data.features[0].properties;
|
||||
// values from api are provided in metric
|
||||
data.observations = observations;
|
||||
data.Temperature = Math.round(observations.temperature.value);
|
||||
data.TemperatureUnit = 'C';
|
||||
data.DewPoint = Math.round(observations.dewpoint.value);
|
||||
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
|
||||
data.CeilingUnit = 'm.';
|
||||
data.Visibility = Math.round(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' km.';
|
||||
data.WindSpeed = Math.round(observations.windSpeed.value);
|
||||
data.WindDirection = directionToNSEW(observations.windDirection.value);
|
||||
data.Pressure = Math.round(observations.barometricPressure.value);
|
||||
data.HeatIndex = Math.round(observations.heatIndex.value);
|
||||
data.WindChill = Math.round(observations.windChill.value);
|
||||
data.WindGust = Math.round(observations.windGust.value);
|
||||
data.WindUnit = 'KPH';
|
||||
data.Humidity = Math.round(observations.relativeHumidity.value);
|
||||
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
||||
data.PressureDirection = '';
|
||||
data.TextConditions = observations.textDescription;
|
||||
data.station = this.data.station;
|
||||
|
||||
// difference since last measurement (pascals, looking for difference of more than 150)
|
||||
const pressureDiff = (observations.barometricPressure.value - this.data.features[1].properties.barometricPressure.value);
|
||||
if (pressureDiff > 150) data.PressureDirection = 'R';
|
||||
if (pressureDiff < -150) data.PressureDirection = 'F';
|
||||
|
||||
data.Temperature = celsiusToFahrenheit(data.Temperature);
|
||||
data.TemperatureUnit = 'F';
|
||||
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
|
||||
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
|
||||
data.CeilingUnit = 'ft.';
|
||||
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' mi.';
|
||||
data.WindSpeed = kphToMph(data.WindSpeed);
|
||||
data.WindUnit = 'MPH';
|
||||
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
|
||||
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
|
||||
data.WindChill = celsiusToFahrenheit(data.WindChill);
|
||||
data.WindGust = kphToMph(data.WindGust);
|
||||
return data;
|
||||
}
|
||||
|
||||
async drawCanvas() {
|
||||
super.drawCanvas();
|
||||
const fill = {};
|
||||
// parse each time to deal with a change in units if necessary
|
||||
const data = this.parseData();
|
||||
|
||||
fill.temp = data.Temperature + String.fromCharCode(176);
|
||||
|
||||
let Conditions = data.observations.textDescription;
|
||||
if (Conditions.length > 15) {
|
||||
Conditions = shortConditions(Conditions);
|
||||
let condition = this.data.observations.textDescription;
|
||||
if (condition.length > 15) {
|
||||
condition = shortConditions(condition);
|
||||
}
|
||||
fill.condition = Conditions;
|
||||
|
||||
fill.wind = data.WindDirection.padEnd(3, '') + data.WindSpeed.toString().padStart(3, ' ');
|
||||
if (data.WindGust) fill['wind-gusts'] = `Gusts to ${data.WindGust}`;
|
||||
const fill = {
|
||||
temp: this.data.Temperature + String.fromCharCode(176),
|
||||
condition,
|
||||
wind: this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' '),
|
||||
location: locationCleanup(this.data.station.properties.name).substr(0, 20),
|
||||
humidity: `${this.data.Humidity}%`,
|
||||
dewpoint: this.data.DewPoint + String.fromCharCode(176),
|
||||
ceiling: (this.data.Ceiling === 0 ? 'Unlimited' : this.data.Ceiling + this.data.CeilingUnit),
|
||||
visibility: this.data.Visibility + this.data.VisibilityUnit,
|
||||
pressure: `${this.data.Pressure} ${this.data.PressureDirection}`,
|
||||
icon: { type: 'img', src: this.data.Icon },
|
||||
};
|
||||
|
||||
fill.location = locationCleanup(this.data.station.properties.name).substr(0, 20);
|
||||
if (this.data.WindGust) fill['wind-gusts'] = `Gusts to ${this.data.WindGust}`;
|
||||
|
||||
fill.humidity = `${data.Humidity}%`;
|
||||
fill.dewpoint = data.DewPoint + String.fromCharCode(176);
|
||||
fill.ceiling = (data.Ceiling === 0 ? 'Unlimited' : data.Ceiling + data.CeilingUnit);
|
||||
fill.visibility = data.Visibility + data.VisibilityUnit;
|
||||
fill.pressure = `${data.Pressure} ${data.PressureDirection}`;
|
||||
|
||||
if (data.observations.heatIndex.value && data.HeatIndex !== data.Temperature) {
|
||||
if (this.data.observations.heatIndex.value && this.data.HeatIndex !== this.data.Temperature) {
|
||||
fill['heat-index-label'] = 'Heat Index:';
|
||||
fill['heat-index'] = data.HeatIndex + String.fromCharCode(176);
|
||||
} else if (data.observations.windChill.value && data.WindChill !== '' && data.WindChill < data.Temperature) {
|
||||
fill['heat-index'] = this.data.HeatIndex + String.fromCharCode(176);
|
||||
} else if (this.data.observations.windChill.value && this.data.WindChill !== '' && this.data.WindChill < this.data.Temperature) {
|
||||
fill['heat-index-label'] = 'Wind Chill:';
|
||||
fill['heat-index'] = data.WindChill + String.fromCharCode(176);
|
||||
fill['heat-index'] = this.data.WindChill + String.fromCharCode(176);
|
||||
}
|
||||
|
||||
fill.icon = { type: 'img', src: data.Icon };
|
||||
|
||||
const area = this.elem.querySelector('.main');
|
||||
|
||||
area.innerHTML = '';
|
||||
@@ -180,9 +129,9 @@ class CurrentWeather extends WeatherDisplay {
|
||||
async getCurrentWeather(stillWaiting) {
|
||||
if (stillWaiting) this.stillWaitingCallbacks.push(stillWaiting);
|
||||
return new Promise((resolve) => {
|
||||
if (this.data) resolve(this.parseData());
|
||||
if (this.data) resolve(this.data);
|
||||
// data not available, put it into the data callback queue
|
||||
this.getDataCallbacks.push(() => resolve(this.parseData()));
|
||||
this.getDataCallbacks.push(() => resolve(this.data));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -206,6 +155,52 @@ const shortConditions = (_condition) => {
|
||||
return condition;
|
||||
};
|
||||
|
||||
// format the received data
|
||||
const parseData = (data) => {
|
||||
const observations = data.features[0].properties;
|
||||
// values from api are provided in metric
|
||||
data.observations = observations;
|
||||
data.Temperature = Math.round(observations.temperature.value);
|
||||
data.TemperatureUnit = 'C';
|
||||
data.DewPoint = Math.round(observations.dewpoint.value);
|
||||
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
|
||||
data.CeilingUnit = 'm.';
|
||||
data.Visibility = Math.round(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' km.';
|
||||
data.WindSpeed = Math.round(observations.windSpeed.value);
|
||||
data.WindDirection = directionToNSEW(observations.windDirection.value);
|
||||
data.Pressure = Math.round(observations.barometricPressure.value);
|
||||
data.HeatIndex = Math.round(observations.heatIndex.value);
|
||||
data.WindChill = Math.round(observations.windChill.value);
|
||||
data.WindGust = Math.round(observations.windGust.value);
|
||||
data.WindUnit = 'KPH';
|
||||
data.Humidity = Math.round(observations.relativeHumidity.value);
|
||||
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
||||
data.PressureDirection = '';
|
||||
data.TextConditions = observations.textDescription;
|
||||
|
||||
// difference since last measurement (pascals, looking for difference of more than 150)
|
||||
const pressureDiff = (observations.barometricPressure.value - data.features[1].properties.barometricPressure.value);
|
||||
if (pressureDiff > 150) data.PressureDirection = 'R';
|
||||
if (pressureDiff < -150) data.PressureDirection = 'F';
|
||||
|
||||
// convert to us units
|
||||
data.Temperature = celsiusToFahrenheit(data.Temperature);
|
||||
data.TemperatureUnit = 'F';
|
||||
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
|
||||
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
|
||||
data.CeilingUnit = 'ft.';
|
||||
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' mi.';
|
||||
data.WindSpeed = kphToMph(data.WindSpeed);
|
||||
data.WindUnit = 'MPH';
|
||||
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
|
||||
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
|
||||
data.WindChill = celsiusToFahrenheit(data.WindChill);
|
||||
data.WindGust = kphToMph(data.WindGust);
|
||||
return data;
|
||||
};
|
||||
|
||||
const display = new CurrentWeather(1, 'current-weather');
|
||||
registerDisplay(display);
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
const fill = {
|
||||
icon: { type: 'img', src: Day.icon },
|
||||
condition: Day.text,
|
||||
date: Day.dayName,
|
||||
};
|
||||
fill.date = Day.dayName;
|
||||
|
||||
const { low } = Day;
|
||||
if (low !== undefined) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
|
||||
class HourlyGraph extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
// special height and width for scrolling
|
||||
super(navId, elemId, 'Hourly Graph', defaultActive);
|
||||
|
||||
// move the top right data into the correct location on load
|
||||
|
||||
@@ -123,10 +123,15 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
|
||||
case 'tropical_storm':
|
||||
return addPath('Thunderstorm.gif');
|
||||
|
||||
case 'wind':
|
||||
case 'wind_few':
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
case 'wind-n':
|
||||
case 'wind_few-n':
|
||||
case 'wind_bkn-n':
|
||||
case 'wind_ovc-n':
|
||||
return addPath('Wind.gif');
|
||||
|
||||
case 'wind_skc':
|
||||
@@ -207,6 +212,9 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
return addPath('CC_Fog.gif');
|
||||
|
||||
case 'rain_sleet':
|
||||
case 'rain_sleet-n':
|
||||
case 'sleet':
|
||||
case 'sleet-n':
|
||||
return addPath('Sleet.gif');
|
||||
|
||||
case 'rain_showers':
|
||||
@@ -242,6 +250,8 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
case 'snow_fzra-n':
|
||||
case 'fzra':
|
||||
case 'fzra-n':
|
||||
case 'rain_fzra':
|
||||
case 'rain_fzra-n':
|
||||
return addPath('CC_FreezingRain.gif');
|
||||
|
||||
case 'snow_sleet':
|
||||
@@ -265,9 +275,10 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
|
||||
case 'wind_sct':
|
||||
case 'wind_bkn':
|
||||
case 'wind_ovc':
|
||||
return addPath('CC_Windy.gif');
|
||||
|
||||
case 'wind_skc':
|
||||
case 'wind_few-n':
|
||||
case 'wind_bkn-n':
|
||||
case 'wind_ovc-n':
|
||||
case 'wind_skc-n':
|
||||
case 'wind_sct-n':
|
||||
return addPath('CC_Windy.gif');
|
||||
|
||||
@@ -112,6 +112,12 @@ const updateStatus = (value) => {
|
||||
value.status = displays[firstDisplayIndex].status;
|
||||
}
|
||||
|
||||
// if hazards data arrives after the firstDisplayIndex loads, then we need to hot wire this to the first display
|
||||
if (value.id === 0 && value.status === STATUS.loaded && displays[0].timing.totalScreens === 0) {
|
||||
value.id = firstDisplayIndex;
|
||||
value.status = displays[firstDisplayIndex].status;
|
||||
}
|
||||
|
||||
// if this is the first display and we're playing, load it up so it starts playing
|
||||
if (isPlaying() && value.id === firstDisplayIndex && value.status === STATUS.loaded) {
|
||||
navTo(msg.command.firstFrame);
|
||||
|
||||
1
tests/README.md
Normal file
1
tests/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Currently, tests take a different approach from typical unit testing. The test methodology loads several forecasts for different locations and logs them all to one logger so errors can be found such as missing icons, locations that do not have all of the necessary data or other changes that may occur between geographical locations.
|
||||
42
tests/index.js
Normal file
42
tests/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
const { setTimeout } = require('node:timers/promises');
|
||||
const { readFile } = require('fs/promises');
|
||||
const messageFormatter = require('./messageformatter');
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
// headless: false,
|
||||
slowMo: 10,
|
||||
timeout: 10_000,
|
||||
dumpio: true,
|
||||
});
|
||||
|
||||
// get the list of locations
|
||||
const LOCATIONS = JSON.parse(await readFile('./tests/locations.json'));
|
||||
|
||||
// get the page
|
||||
const page = (await browser.pages())[0];
|
||||
await page.goto('http://localhost:8080');
|
||||
|
||||
page.on('console', messageFormatter);
|
||||
|
||||
// run all the locations
|
||||
for (let i = 0; i < LOCATIONS.length; i += 1) {
|
||||
const location = LOCATIONS[i];
|
||||
console.log(location);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await tester(location, page);
|
||||
}
|
||||
|
||||
browser.close();
|
||||
})();
|
||||
|
||||
const tester = async (location, page) => {
|
||||
// Set the address
|
||||
await page.type('#txtAddress', location);
|
||||
await setTimeout(500);
|
||||
// get the page
|
||||
await page.click('#btnGetLatLng');
|
||||
// wait for errors
|
||||
await setTimeout(5000);
|
||||
};
|
||||
52
tests/locations.json
Normal file
52
tests/locations.json
Normal file
@@ -0,0 +1,52 @@
|
||||
[
|
||||
"New York, New York",
|
||||
"Los Angeles, California",
|
||||
"Chicago, Illinois",
|
||||
"Houston, Texas",
|
||||
"Phoenix, Arizona",
|
||||
"Philadelphia, Pennsylvania",
|
||||
"San Antonio, Texas",
|
||||
"San Diego, California",
|
||||
"Dallas, Texas",
|
||||
"San Jose, California",
|
||||
"Austin, Texas",
|
||||
"Jacksonville, Florida",
|
||||
"Fort Worth, Texas",
|
||||
"Columbus, Ohio",
|
||||
"Charlotte, North Carolina",
|
||||
"Indianapolis, Indiana",
|
||||
"San Francisco, California",
|
||||
"Seattle, Washington",
|
||||
"Denver, Colorado",
|
||||
"Nashville, Tennessee",
|
||||
"Washington, District of Columbia",
|
||||
"Oklahoma City, Oklahoma",
|
||||
"Boston, Massachusetts",
|
||||
"El Paso, Texas",
|
||||
"Portland, Oregon",
|
||||
"Las Vegas, Nevada",
|
||||
"Memphis, Tennessee",
|
||||
"Detroit, Michigan",
|
||||
"Baltimore, Maryland",
|
||||
"Milwaukee, Wisconsin",
|
||||
"Albuquerque, New Mexico",
|
||||
"Fresno, California",
|
||||
"Tucson, Arizona",
|
||||
"Sacramento, California",
|
||||
"Mesa, Arizona",
|
||||
"Kansas City, Missouri",
|
||||
"Atlanta, Georgia",
|
||||
"Omaha, Nebraska",
|
||||
"Colorado Springs, Colorado",
|
||||
"Raleigh, North Carolina",
|
||||
"Long Beach, California",
|
||||
"Virginia Beach, Virginia",
|
||||
"Oakland, California",
|
||||
"Miami, Florida",
|
||||
"Minneapolis, Minnesota",
|
||||
"Bakersfield, California",
|
||||
"Tulsa, Oklahoma",
|
||||
"Aurora, Colorado",
|
||||
"Arlington, Texas",
|
||||
"Wichita, Kansas"
|
||||
]
|
||||
28
tests/messageformatter.js
Normal file
28
tests/messageformatter.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const chalk = require('chalk');
|
||||
|
||||
const describe = (jsHandle) => jsHandle.executionContext().evaluate(
|
||||
// serialize |obj| however you want
|
||||
(obj) => `OBJ: ${typeof obj}, ${obj}`,
|
||||
jsHandle,
|
||||
);
|
||||
|
||||
const colors = {
|
||||
LOG: chalk.grey,
|
||||
ERR: chalk.red,
|
||||
WAR: chalk.yellow,
|
||||
INF: chalk.cyan,
|
||||
};
|
||||
|
||||
const formatter = async (message) => {
|
||||
const args = await Promise.all(message.args().map((arg) => describe(arg)));
|
||||
// make ability to paint different console[types]
|
||||
const type = message.type().substr(0, 3).toUpperCase();
|
||||
const color = colors[type] || chalk.blue;
|
||||
let text = '';
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
text += `[${i}] ${args[i]} `;
|
||||
}
|
||||
console.log(color(`CONSOLE.${type}: ${message.text()}\n${text} `));
|
||||
};
|
||||
|
||||
module.exports = formatter;
|
||||
1436
tests/package-lock.json
generated
Normal file
1436
tests/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
tests/package.json
Normal file
15
tests/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "ws4kp-tests",
|
||||
"version": "1.0.0",
|
||||
"description": "Currently, tests take a different approach from typical unit testing. The test methodology loads several forecasts for different locations and logs them all to one logger so errors can be found such as missing icons, locations that do not have all of the necessary data or other changes that may occur between geographical locations.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^4.0.0",
|
||||
"puppeteer": "^19.5.2"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user