diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 6b40f57..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,202 +0,0 @@ -/* -👋 Hi! This file was autogenerated by tslint-to-eslint-config. -https://github.com/typescript-eslint/tslint-to-eslint-config - -It represents the closest reasonable ESLint configuration to this -project's original TSLint configuration. - -We recommend eventually switching this configuration to extend from -the recommended rulesets in typescript-eslint. -https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md - -Happy linting! 💖 -*/ -module.exports = { - env: { - browser: true, - es6: true, - node: true - }, - extends: ['plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking'], - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - sourceType: 'module' - }, - plugins: ['eslint-plugin-import', 'eslint-plugin-jsdoc', 'eslint-plugin-prefer-arrow', '@typescript-eslint'], - rules: { - '@typescript-eslint/adjacent-overload-signatures': 'error', - '@typescript-eslint/array-type': [ - 'error', - { - default: 'array' - } - ], - '@typescript-eslint/ban-types': [ - 'error', - { - types: { - Object: { - message: 'Avoid using the `Object` type. Did you mean `object`?' - }, - Function: { - message: 'Avoid using the `Function` type. Prefer a specific function type, like `() => void`.' - }, - Boolean: { - message: 'Avoid using the `Boolean` type. Did you mean `boolean`?' - }, - Number: { - message: 'Avoid using the `Number` type. Did you mean `number`?' - }, - String: { - message: 'Avoid using the `String` type. Did you mean `string`?' - }, - Symbol: { - message: 'Avoid using the `Symbol` type. Did you mean `symbol`?' - } - } - } - ], - '@typescript-eslint/consistent-type-assertions': 'error', - '@typescript-eslint/dot-notation': 'error', - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { - allowExpressions: false, - allowTypedFunctionExpressions: false, - allowHigherOrderFunctions: false, - allowDirectConstAssertionInArrowFunctions: true, - allowConciseArrowFunctionExpressionsStartingWithVoid: true - } - ], - '@typescript-eslint/explicit-member-accessibility': [ - 'error', - { - accessibility: 'explicit', - overrides: { - accessors: 'explicit' - } - } - ], - '@typescript-eslint/explicit-module-boundary-types': [ - 'error', - { - allowArgumentsExplicitlyTypedAsAny: true, - allowDirectConstAssertionInArrowFunctions: true, - allowHigherOrderFunctions: false, - allowTypedFunctionExpressions: false - } - ], - '@typescript-eslint/naming-convention': 'error', - '@typescript-eslint/no-empty-function': 'error', - '@typescript-eslint/no-empty-interface': 'warn', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'error', - '@typescript-eslint/no-misused-new': 'error', - '@typescript-eslint/no-namespace': 'error', - '@typescript-eslint/no-parameter-properties': 'off', - '@typescript-eslint/no-shadow': [ - 'error', - { - hoist: 'all' - } - ], - '@typescript-eslint/no-unnecessary-type-assertion': 'error', - '@typescript-eslint/no-unused-expressions': 'error', - '@typescript-eslint/no-use-before-define': 'off', - '@typescript-eslint/no-var-requires': 'error', - '@typescript-eslint/prefer-for-of': 'warn', - '@typescript-eslint/prefer-function-type': 'error', - '@typescript-eslint/prefer-namespace-keyword': 'error', - '@typescript-eslint/quotes': ['error', 'single'], - '@typescript-eslint/triple-slash-reference': [ - 'error', - { - path: 'always', - types: 'prefer-import', - lib: 'always' - } - ], - '@typescript-eslint/typedef': [ - 'error', - { - parameter: true - } - ], - '@typescript-eslint/unified-signatures': 'warn', - 'comma-dangle': 'off', - complexity: 'off', - 'constructor-super': 'error', - 'dot-notation': 'error', - eqeqeq: ['warn', 'always'], - 'guard-for-in': 'error', - 'id-denylist': [ - 'error', - 'any', - 'Number', - 'number', - 'String', - 'string', - 'Boolean', - 'boolean', - 'Undefined', - 'undefined' - ], - 'id-match': 'error', - 'import/no-deprecated': 'warn', - 'import/no-extraneous-dependencies': 'off', - 'import/no-unassigned-import': 'warn', - 'jsdoc/check-alignment': 'error', - 'jsdoc/check-indentation': 'error', - 'jsdoc/newline-after-description': 'error', - 'max-classes-per-file': ['error', 1], - 'new-parens': 'error', - 'no-bitwise': 'error', - 'no-caller': 'error', - 'no-cond-assign': 'error', - 'no-console': 'off', - 'no-debugger': 'error', - 'no-duplicate-case': 'error', - 'no-duplicate-imports': 'error', - 'no-empty': [ - 'error', - { - allowEmptyCatch: true - } - ], - 'no-empty-function': 'error', - 'no-eval': 'error', - 'no-fallthrough': 'off', - 'no-invalid-this': 'error', - 'no-new-wrappers': 'error', - 'no-param-reassign': 'error', - 'no-redeclare': 'error', - 'no-sequences': 'error', - 'no-shadow': 'error', - 'no-throw-literal': 'error', - 'no-trailing-spaces': 'error', - 'no-undef-init': 'error', - 'no-underscore-dangle': 'error', - 'no-unsafe-finally': 'error', - 'no-unused-expressions': 'error', - 'no-unused-labels': 'error', - 'no-use-before-define': 'off', - 'no-var': 'warn', - 'no-void': 'error', - 'object-shorthand': 'error', - 'one-var': ['error', 'never'], - 'prefer-arrow/prefer-arrow-functions': 'error', - 'prefer-const': 'warn', - quotes: 'error', - radix: 'error', - 'spaced-comment': [ - 'error', - 'always', - { - markers: ['/'] - } - ], - 'use-isnan': 'error', - 'valid-typeof': 'off' - } -}; diff --git a/.github/actions/cached-deps/action.yml b/.github/actions/cached-deps/action.yml index 660cbbc..90793c3 100644 --- a/.github/actions/cached-deps/action.yml +++ b/.github/actions/cached-deps/action.yml @@ -1,39 +1,8 @@ -name: Get & Cache Dependencies -description: Get dependencies via yarn and cache them -inputs: - caching: - description: Whether to cache dependencies or not - required: false - default: 'true' -outputs: - used-cache: - description: Whether the cache was used - value: ${{ steps.install.outputs.cache }} +name: Install Dependencies +description: Install dependencies from the frozen Yarn lockfile runs: using: composite steps: - - name: Cache dependencies - if: inputs.caching == 'true' - id: cache - uses: actions/cache@v3 - with: - path: node_modules - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.OS }}-yarn-${{ env.cache-name }}- - ${{ runner.OS }}-yarn- - ${{ runner.OS }}- - - name: Install - if: steps.cache.outputs.cache-hit != 'true' || inputs.caching != 'true' - id: install - run: | - yarn install --frozen-lockfile - echo "cache='${{ inputs.caching }}'" >> $GITHUB_OUTPUT - shell: bash - - - name: Postinstall - if: steps.cache.outputs.cache-hit == 'true' && inputs.caching == 'true' - id: postinstall - run: yarn postinstall + run: yarn install --frozen-lockfile shell: bash diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ace66ab..a4adbf6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,6 +5,10 @@ on: tags: - 'v*' +permissions: + contents: read + packages: write + jobs: install-and-build: name: 🛠️ Install & Build @@ -17,7 +21,8 @@ jobs: - name: 🗜️ Setup node version uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .nvmrc + cache: yarn registry-url: 'https://registry.npmjs.org' - name: 🛠️ Install @@ -26,16 +31,19 @@ jobs: - name: 📦 Build run: yarn build + - name: 🧹 Run lint + run: yarn lint + - name: 🧪 Run tests run: yarn test --coverage --run - - name: ⬆️ Upload coverage reports - uses: codecov/codecov-action@v6 - with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - fail_ci_if_error: true - verbose: false - dry_run: false + # - name: ⬆️ Upload coverage reports + # uses: codecov/codecov-action@v6 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + # fail_ci_if_error: true + # verbose: false + # dry_run: false - name: 🚀 Publish NPM if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, 'beta') == false @@ -58,7 +66,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') && contains(github.ref, 'beta') == false uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .nvmrc registry-url: https://npm.pkg.github.com/ scope: '@celtian' diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 8c470d8..8e702dc 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,6 +1,10 @@ name: 🐛 Test feature branch 'on': pull_request +permissions: + contents: read + pull-requests: write + jobs: install-and-test: name: 🛠️ Install & Test @@ -13,7 +17,8 @@ jobs: - name: 🗜️ Setup node version uses: actions/setup-node@v6 with: - node-version: 22 + node-version-file: .nvmrc + cache: yarn registry-url: 'https://registry.npmjs.org' - name: 🛠️ Install @@ -22,6 +27,9 @@ jobs: - name: 📦 Build run: yarn build + - name: 🧹 Run lint + run: yarn lint + - name: 🧪 Run tests run: yarn test --coverage --run @@ -33,10 +41,10 @@ jobs: json-summary-path: ./coverage/coverage-summary.json json-final-path: ./coverage/coverage-final.json - - name: ⬆️ Upload coverage reports - uses: codecov/codecov-action@v6 - with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - fail_ci_if_error: true - verbose: true - dry_run: true + # - name: ⬆️ Upload coverage reports + # uses: codecov/codecov-action@v6 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + # fail_ci_if_error: true + # verbose: true + # dry_run: true diff --git a/.husky/commit-msg b/.husky/commit-msg index 5e563eb..70bd3dd 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx --no-install commitlint --edit +npx --no-install commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit index c537582..6c66e07 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,2 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx pretty-quick --staged - +npx --no-install lint-staged --relative +npm test diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..4dbfde8 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "*.{js,cjs,mjs,ts}": ["eslint --fix", "prettier --write"], + "*.{json,md,yml,yaml}": "prettier --write" +} diff --git a/.nvmrc b/.nvmrc index 2bd5a0a..a45fd52 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 +24 diff --git a/README.md b/README.md index b6096b6..2f00883 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,19 @@ _Type this into your ts file._ ```terminal import { soccerway, transfermarkt, fotbalunas, eurofotbal, sportnet } from 'soccerbot'; -console.log(soccerway.teamUrl('533')); // show url +console.log(soccerway.leagueUrl('czech-republic/chance-liga/standings/bNFMkskm')); // show url -soccerway.team('533') +soccerway.league('czech-republic/chance-liga/standings/bNFMkskm') + .then(res => console.log(res)); + +console.log(soccerway.teamUrl('slavia-prague/viXGgnyB')); // show url + +soccerway.team('slavia-prague/viXGgnyB') + .then(res => console.log(res)); + +console.log(soccerway.playerUrl('kolar-ondrej/xfBGcS1U')); // show url + +soccerway.player('kolar-ondrej/xfBGcS1U') .then(res => console.log(res)); console.log(transfermarkt.teamUrl('62')); // show url @@ -55,12 +65,21 @@ console.log(eurofotbal.teamUrl('cesko/sparta-praha')); // show url eurofotbal.team('cesko/sparta-praha') .then(res => console.log(res)); -console.log(sportnet.teamUrl('fk-inter-bratislava/tim/46770')); // show url +console.log(sportnet.teamUrl('fk-inter-bratislava-1/tim/dospeli-m-a')); // show url -sportnet.team('fk-inter-bratislava/tim/46770') +sportnet.team('fk-inter-bratislava-1/tim/dospeli-m-a') .then(res => console.log(res)); ``` +Transfermarkt players also include an optional FIFA-style detailed position: + +```typescript +import { SoccerBotPositionDetail, transfermarkt } from 'soccerbot'; + +const response = await transfermarkt.team('62'); +const centreBacks = response.data?.filter((player) => player.positionDetail === SoccerBotPositionDetail.CB); +``` + ## 🛠️ Sources | Source | Speed | Stability | Coverage | diff --git a/data/source.csv b/data/source.csv index d123272..23a150c 100644 --- a/data/source.csv +++ b/data/source.csv @@ -48,7 +48,7 @@ Croatia;HR;HRV;37;Croatia;Chorvatsko;Chorvátsko Cuba;CU;CUB;88;Cuba;Kuba;Kuba Curaçao;CW;CUW;260;Curaçao;Curaçao;Curaçao Cyprus;CY;CYP;188;Cyprus;Kypr;Cyprus -Czech Republic;CZ;CZE;172;Czechia;Česko;Česko +Czech Republic;CZ;CZE;172;Czech Republic;Česko;Česko Denmark;DK;DNK;39;Denmark;Dánsko;Dánsko Djibouti;DJ;DJI;41;Djibouti;Džibutsko;Džibutsko Dominica;DM;DMA;42;Dominica;Dominika;Dominika diff --git a/demo.ts b/demo.ts index 5e584ac..77cb09c 100644 --- a/demo.ts +++ b/demo.ts @@ -18,14 +18,14 @@ const transfermarktDemo = async (): Promise => { const soccerwayDemo = async (): Promise => { console.log('⚽⚽⚽ Soccerway demo ⚽⚽⚽'); - console.log(soccerway.leagueUrl('r59164', '20202021')); // CZE-1 - season 2020 - 2021 - console.log(await soccerway.league('r59164', '20202021')); + console.log(soccerway.leagueUrl('czech-republic/chance-liga/standings/bNFMkskm')); // CZE-1 + console.log(await soccerway.league('czech-republic/chance-liga/standings/bNFMkskm')); - console.log(soccerway.teamUrl('533')); // Slavia Prague - current season - console.log(await soccerway.team('533')); + console.log(soccerway.teamUrl('slavia-prague/viXGgnyB')); // Slavia Prague - current season + console.log(await soccerway.team('slavia-prague/viXGgnyB')); - console.log(soccerway.playerUrl('193498')); // player - console.log(await soccerway.player('193498')); + console.log(soccerway.playerUrl('kolar-ondrej/xfBGcS1U')); // Ondrej Kolar + console.log(await soccerway.player('kolar-ondrej/xfBGcS1U')); }; const fotbalunasDemo = async (): Promise => { @@ -41,8 +41,8 @@ const fotbalunasDemo = async (): Promise => { const eurofotbalDemo = async (): Promise => { console.log('⚽⚽⚽ Eurofotbal demo ⚽⚽⚽'); - console.log(eurofotbal.leagueUrl('fortuna-liga-cz')); - console.log(await eurofotbal.league('fortuna-liga-cz')); + console.log(eurofotbal.leagueUrl('chance-liga')); + console.log(await eurofotbal.league('chance-liga')); console.log(eurofotbal.teamUrl('cesko/slavia-praha')); console.log(await eurofotbal.team('cesko/slavia-praha')); @@ -51,11 +51,11 @@ const eurofotbalDemo = async (): Promise => { const sportnetDemo = async (): Promise => { console.log('⚽⚽⚽ Sportnet demo ⚽⚽⚽'); - console.log(sportnet.leagueUrl('sfz/s/3528')); - console.log(await sportnet.league('sfz/s/3528')); + console.log(sportnet.leagueUrl('sfz/s/monacobet-liga')); + console.log(await sportnet.league('sfz/s/monacobet-liga')); - console.log(sportnet.teamUrl('fk-inter-bratislava/tim/46770')); - console.log(await sportnet.team('fk-inter-bratislava/tim/46770')); + console.log(sportnet.teamUrl('fk-inter-bratislava-1/tim/dospeli-m-a')); + console.log(await sportnet.team('fk-inter-bratislava-1/tim/dospeli-m-a')); console.log(sportnet.playerUrl('1306959')); console.log(await sportnet.player('1306959')); diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..af6f554 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,16 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['coverage/**', 'dist/**', 'node_modules/**', 'test/mocks/**'] + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off' + } + } +); diff --git a/package.json b/package.json index cb65e33..03e7d3e 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "demo": "yarn tsx ./demo.ts", "build": "rimraf dist && tsc --build tsconfig.es6.json && tsc --build tsconfig.es5.json", "postbuild": "copyfiles ./package.json ./README.md ./LICENSE ./dist && yarn prepare-dist", + "lint": "eslint .", "test": "vitest run", - "husky:install": "husky", "prepare-source": "yarn tsx ./scripts/prepare-source.ts", "prepare-dist": "yarn tsx ./scripts/prepare-dist.ts", "gpr:setup": "yarn tsx ./scripts/gpr-setup.ts", @@ -20,7 +20,8 @@ "release:patch": "git checkout master && npm version patch -m \"chore(update): patch release %s 🐛 \"", "release:minor": "git checkout master && npm version minor -m \"chore(update): release %s 🚀\"", "release:major": "git checkout master && npm version major -m \"chore(update): major release %s 💥 \"", - "fix-vulnerabilities": "npx yarn-audit-fix && npx browserslist --update-db" + "fix-vulnerabilities": "npx yarn-audit-fix && npx browserslist --update-db", + "prepare": "husky" }, "repository": { "type": "git", @@ -34,38 +35,47 @@ "bugs": { "url": "https://github.com/Celtian/soccerbot/issues" }, + "auto-changelog": { + "remote": "none", + "commitUrl": "https://github.com/Celtian/soccerbot/commit/{id}", + "issueUrl": "https://github.com/Celtian/soccerbot/issues/{id}", + "mergeUrl": "https://github.com/Celtian/soccerbot/pull/{id}", + "compareUrl": "https://github.com/Celtian/soccerbot/compare/{from}...{to}" + }, "homepage": "https://github.com/Celtian/soccerbot#readme", "engines": { - "node": ">= 12", - "npm": "please-use-yarn" + "node": ">=22.13.0" }, + "packageManager": "yarn@1.22.22", "publishConfig": { "registry": "https://registry.npmjs.org" }, "dependencies": { "cross-fetch": "^4.1.0", - "node-html-parser": "^6.1.13" + "node-html-parser": "^9.0.0" }, "devDependencies": { - "@commitlint/cli": "^20.5.3", - "@commitlint/config-conventional": "^20.5.3", + "@commitlint/cli": "^21.2.1", + "@commitlint/config-conventional": "^21.2.0", + "@eslint/js": "^10.0.1", "@types/fs-extra": "^11.0.4", - "@typescript-eslint/eslint-plugin": "^8.48.1", - "@typescript-eslint/parser": "^8.48.1", - "@vitest/coverage-v8": "^4.1.5", - "auto-changelog": "^2.5.0", + "@types/node": "^24.13.3", + "@vitest/coverage-v8": "^4.1.10", + "auto-changelog": "^2.6.0", "copyfiles": "^2.4.1", - "eslint": "^9.39.1", - "fs-extra": "^11.3.4", + "eslint": "^10.7.0", + "fs-extra": "^11.3.6", "husky": "^9.1.7", - "prettier": "^3.8.3", - "pretty-quick": "^4.2.2", + "lint-staged": "^16.4.0", + "prettier": "^3.9.6", "rimraf": "^6.1.3", "ts-node": "^10.9.2", - "tsc-watch": "^6.2.0", - "tsx": "^4.21.0", - "typescript": "^5.4.5", - "vitest": "^4.1.5" + "tsc-watch": "^7.2.1", + "tsx": "^4.23.1", + "typescript": "^6.0.3", + "typescript-eslint": "^8.65.0", + "vite": "^8.1.5", + "vitest": "^4.1.10" }, "keywords": [ "data", diff --git a/scripts/prepare-dist.ts b/scripts/prepare-dist.ts index 4e9c1af..aeca702 100644 --- a/scripts/prepare-dist.ts +++ b/scripts/prepare-dist.ts @@ -10,7 +10,6 @@ pkg.devDependencies = {}; pkg.main = 'es5/index.js'; pkg.module = 'es6/index.js'; pkg.sideEffects = false; -pkg.engines.npm = undefined; writeFileSync(join(__dirname, '..', 'dist', 'package.json'), JSON.stringify(pkg, null, 2)); console.log('File package.json modified:', pkg); diff --git a/src/clients/eurofotbal/index.ts b/src/clients/eurofotbal/index.ts index c596df2..a27aa5c 100644 --- a/src/clients/eurofotbal/index.ts +++ b/src/clients/eurofotbal/index.ts @@ -17,7 +17,7 @@ export class SoccerBotEurofotbalClient extends SoccerBotClient { if (!id) { return undefined; } - return `${BASE_URL}/${id}`; + return `${BASE_URL}/${id}/tabulky/`; } public teamUrl(id: string): string { diff --git a/src/clients/soccerway/index.ts b/src/clients/soccerway/index.ts index 1cadd0a..7283ca4 100644 --- a/src/clients/soccerway/index.ts +++ b/src/clients/soccerway/index.ts @@ -1,10 +1,10 @@ import parse from 'node-html-parser'; -import { coerceCountry, coerceDate, coerceFoot, coercePositionGroup, sleep } from '../../helpers'; -import { coerceHeight, coerceJerseyNumber, coerceMinutesPlayed, coerceWeight } from '../../helpers/number'; +import { coerceCountry, coerceDate, coercePositionGroup, sleep } from '../../helpers'; +import { coerceJerseyNumber, coerceMinutesPlayed } from '../../helpers/number'; import { SoccerBotPlayer, SoccerBotProvider, SoccerBotResponse, SoccerBotTeam } from '../../shared/interfaces'; import { SoccerBotClient, UserAgents } from '../shared'; -const BASE_URL = 'https://int.soccerway.com'; +const BASE_URL = 'https://www.soccerway.com'; export class SoccerBotSoccerwayClient extends SoccerBotClient { protected userAgents: UserAgents[] = [ @@ -20,46 +20,61 @@ export class SoccerBotSoccerwayClient extends SoccerBotClient { super(); } - public leagueUrl(id: string, season: string): string { - if (!id || !season) { + public leagueUrl(id: string): string { + if (!id) { return undefined; } - return `${BASE_URL}/national/country-slug/team-slug/${season}/regular-season/${id}/tables/`; + return `${BASE_URL}/${id}/standings/overall/`; } public teamUrl(id: string): string { if (!id) { return undefined; } - return `${BASE_URL}/teams/country-slug/team-slug/${id}/squad/`; + return `${BASE_URL}/team/${id}/squad/`; } public playerUrl(id: string): string { if (!id) { return undefined; } - return `${BASE_URL}/players/player-slug/${id}/`; + return `${BASE_URL}/player/${id}/`; } - public async league(id: string, season: string): Promise> { + public async league(id: string): Promise> { try { - const url = this.leagueUrl(id, season); - const html = parse(await this.fetchPage(url)); - const items = html.querySelectorAll('table[data-round_id].detailed-table > tbody > tr'); - const list: SoccerBotTeam[] = []; - for (const item of items) { - const link = item.querySelector('td.text.team.large-link > a'); - list.push({ - id: link - .getAttribute('href') - .trim() - .match(/^(.*)\/(?\d+)(\/)?$/).groups.id, - name: link.text.trim() - }); + const page = await this.fetchPage(this.leagueUrl(id)); + const feed = page.match(/data:\s*`(?SA÷[^`]+)`/s)?.groups?.data; + if (!feed) { + throw new Error('Soccerway league feed was not found'); + } + + const teams = new Map(); + for (const event of feed.split('¬~AA÷').slice(1)) { + const fields = new Map(); + for (const field of event.split('¬')) { + const separator = field.indexOf('÷'); + if (separator > 0) { + fields.set(field.slice(0, separator), field.slice(separator + 1)); + } + } + + for (const [idKey, nameKey, slugKey] of [ + ['PX', 'AE', 'WU'], + ['PY', 'AF', 'WV'] + ]) { + const teamId = fields.get(idKey); + const name = fields.get(nameKey); + const slug = fields.get(slugKey); + if (teamId && name && slug && !teams.has(teamId)) { + teams.set(teamId, { id: `${slug}/${teamId}`, name }); + } + } } + return { ok: true, - data: list + data: [...teams.values()] }; } catch (error) { return { @@ -72,16 +87,18 @@ export class SoccerBotSoccerwayClient extends SoccerBotClient { public async team(id: string): Promise> { try { const html = parse(await this.fetchPage(this.teamUrl(id))); - const items = html.querySelectorAll('table[data-season_id] > tbody > tr'); + const items = html.querySelectorAll('#overall-all-table .lineupTable__row'); const list: SoccerBotPlayer[] = []; for (const item of items) { - const link = item.querySelector('td.name.large-link > a'); + const link = item.querySelector('.lineupTable__cell--name'); const id = link .getAttribute('href') .trim() - .match(/^(.*)\/(?\d+)(\/)?$/).groups.id; - const jerseyNumber = coerceJerseyNumber(item.querySelector('td.shirtnumber')?.text?.trim()); - const minutesPlayed = coerceMinutesPlayed(item.querySelector('td.game-minutes')?.text?.trim()); + .match(/^\/player\/(?[^/]+\/[^/]+)\/?$/).groups.id; + const jerseyNumber = coerceJerseyNumber(item.querySelector('.lineupTable__cell--jersey')?.text?.trim()); + const minutesPlayed = coerceMinutesPlayed( + item.querySelector('.lineupTable__cell--minutesPlayed')?.text?.trim() + ); await sleep(this.sleepMs); // sleep for a moment because of rare limit const player = await this.player(id); list.push({ @@ -107,28 +124,24 @@ export class SoccerBotSoccerwayClient extends SoccerBotClient { try { const url = this.playerUrl(id); const html = parse(await this.fetchPage(url)); - const data = html.querySelector('.block_player_passport > div > div > div.yui-u.first > div.clearfix'); - const firstName = data.querySelector('[data-first_name="first_name"]')?.text?.trim(); - const lastName = data.querySelector('[data-last_name="last_name"]')?.text?.trim(); + const data = html.querySelector('#player-profile-heading'); + const name = data.querySelector('h2')?.text?.trim(); + const [firstName, ...lastNameParts] = name.split(/\s+/); + const lastName = lastNameParts.join(' '); + const age = data + .querySelectorAll('.playerInfoItem') + .find((item) => item.text.trim().startsWith('Age:')) + ?.text?.trim(); return { ok: true, data: { id, - name: `${firstName} ${lastName}`, + name, firstName, lastName, - country: coerceCountry( - data.querySelector('[data-nationality="nationality"]')?.text?.trim(), - SoccerBotProvider.SOCCERWAY - ), - birthdate: coerceDate( - data.querySelector('[data-date_of_birth="date_of_birth"]')?.text?.trim(), - SoccerBotProvider.SOCCERWAY - ), - position: coercePositionGroup(data.querySelector('[data-position="position"]')?.text?.trim()), - height: coerceHeight(data.querySelector('[data-height="height"]')?.text?.trim()), - weight: coerceWeight(data.querySelector('[data-weight="weight"]')?.text?.trim()), - foot: coerceFoot(data.querySelector('[data-foot="foot"]')?.text?.trim()) + country: coerceCountry(data.querySelector('[itemprop="name"]')?.text?.trim(), SoccerBotProvider.SOCCERWAY), + birthdate: coerceDate(age, SoccerBotProvider.SOCCERWAY), + position: coercePositionGroup(data.querySelector('.playerTeam > span')?.text?.trim()) } }; } catch (error) { diff --git a/src/clients/sportnet/index.ts b/src/clients/sportnet/index.ts index d17dcbe..1945975 100644 --- a/src/clients/sportnet/index.ts +++ b/src/clients/sportnet/index.ts @@ -40,12 +40,19 @@ export class SoccerBotSportnetClient extends SoccerBotClient { const items = html.querySelectorAll('table > tbody > tr'); const list: SoccerBotTeam[] = []; for (const item of items) { - const link = item.querySelector('td:nth-child(3) > a'); + const link = item.querySelector('a[href^="/futbalnet/k/"]'); + if (!link) { + continue; + } + const match = link + .getAttribute('href') + .trim() + .match(/^\/futbalnet\/k\/(?.+?)\/?$/); + if (!match) { + continue; + } list.push({ - id: link - .getAttribute('href') - .trim() - .match(/^\/futbalnet\/k\/\b(?.*)\b(\/)?$/).groups.id, + id: match.groups.id, name: link.text.trim() }); } @@ -65,6 +72,12 @@ export class SoccerBotSportnetClient extends SoccerBotClient { try { const html = parse(await this.fetchPage(this.teamUrl(id))); const links = html.querySelectorAll('div:nth-child(2) > div.dropdown-body > a'); + if (!links.length) { + return { + ok: true, + data: [] + }; + } const list: SoccerBotPlayer[] = []; const players = new Map(); for (const link of links) { diff --git a/src/clients/transfermarkt/index.ts b/src/clients/transfermarkt/index.ts index 3373d79..956e6ba 100644 --- a/src/clients/transfermarkt/index.ts +++ b/src/clients/transfermarkt/index.ts @@ -4,7 +4,7 @@ import { coerceDate } from '../../helpers/date'; import { coerceFoot } from '../../helpers/foot'; import { coerceMarketValue } from '../../helpers/market-value'; import { coerceHeight, coerceJerseyNumber } from '../../helpers/number'; -import { coercePositionGroup } from '../../helpers/position'; +import { coercePositionDetail, coercePositionGroup } from '../../helpers/position'; import { SoccerBotPlayer, SoccerBotProvider, SoccerBotResponse, SoccerBotTeam } from '../../shared/interfaces'; import { SoccerBotClient } from '../shared'; @@ -61,6 +61,7 @@ export class SoccerBotTransfermarktClient extends SoccerBotClient { for (const item of items) { // const link = item.querySelector('td.posrela > table > tbody > tr:nth-child(1) > td.hauptlink > a'); const link = item.querySelector('td.posrela > table tr:nth-child(1) > td.hauptlink > a'); + const position = item.querySelector('td:nth-child(2) > table tr:nth-child(2) > td')?.text?.trim(); const flagSrc = item .querySelector('td:nth-child(4) > img:nth-child(1)') ?.getAttribute('src') @@ -73,10 +74,8 @@ export class SoccerBotTransfermarktClient extends SoccerBotClient { ?.match(/\/(\S+)\/profil\/spieler\/(?\d+)$/)?.groups?.id, name: link?.text?.trim(), jerseyNumber: coerceJerseyNumber(item.querySelector('td:nth-child(1) > div.rn_nummer')?.text?.trim()), - // position: coercePositionGroup(item.querySelector('td:nth-child(2) > table > tbody > tr:nth-child(2) > td')?.text?.trim()), - position: coercePositionGroup( - item.querySelector('td:nth-child(2) > table tr:nth-child(2) > td')?.text?.trim() - ), + position: coercePositionGroup(position), + positionDetail: coercePositionDetail(position), birthdate: coerceDate(item.querySelector('td:nth-child(3)')?.text?.trim(), SoccerBotProvider.TRANSFERMARKT), height: coerceHeight(item.querySelector('td:nth-child(5)')?.text?.trim()), foot: coerceFoot(item.querySelector('td:nth-child(6)')?.text?.trim()), diff --git a/src/helpers/date/index.ts b/src/helpers/date/index.ts index 2fbef9a..3b7a6ee 100644 --- a/src/helpers/date/index.ts +++ b/src/helpers/date/index.ts @@ -58,7 +58,10 @@ export const coerceDate = (date: string, type: SoccerBotProvider): string => { return corceDateByRegExp(date, regExp, MONTH_NUMBER); case SoccerBotProvider.SOCCERWAY: regExp = new RegExp('(?\\d{1,2})\\s(?(' + Object.keys(MONTH_LONG).join('|') + '))\\s(?\\d{4})'); - return corceDateByRegExp(date, regExp, MONTH_LONG); + return ( + corceDateByRegExp(date, regExp, MONTH_LONG) || + corceDateByRegExp(date, new RegExp('(?\\d{1,2})\\.(?\\d{1,2})\\.(?\\d{4})'), MONTH_NUMBER) + ); case SoccerBotProvider.EUROFOTBAL: regExp = new RegExp('(?\\d{1,2}).(?\\d{1,2}).(?\\d{4})'); return corceDateByRegExp(date, regExp, MONTH_NUMBER); diff --git a/src/helpers/position/index.ts b/src/helpers/position/index.ts index a20f842..95134bb 100644 --- a/src/helpers/position/index.ts +++ b/src/helpers/position/index.ts @@ -1,4 +1,23 @@ -import { SoccerBotPositionGroup } from '../../shared/interfaces'; +import { SoccerBotPositionDetail, SoccerBotPositionGroup } from '../../shared/interfaces'; + +const POSITION_DETAIL: Record = { + Goalkeeper: SoccerBotPositionDetail.GK, + Sweeper: SoccerBotPositionDetail.SW, + 'Centre-Back': SoccerBotPositionDetail.CB, + 'Left-Back': SoccerBotPositionDetail.LB, + 'Right-Back': SoccerBotPositionDetail.RB, + 'Defensive Midfield': SoccerBotPositionDetail.CDM, + 'Central Midfield': SoccerBotPositionDetail.CM, + 'Right Midfield': SoccerBotPositionDetail.RM, + 'Left Midfield': SoccerBotPositionDetail.LM, + 'Attacking Midfield': SoccerBotPositionDetail.CAM, + 'Left Winger': SoccerBotPositionDetail.LW, + 'Right Winger': SoccerBotPositionDetail.RW, + 'Second Striker': SoccerBotPositionDetail.CF, + 'Centre-Forward': SoccerBotPositionDetail.ST +}; + +export const coercePositionDetail = (position: string): SoccerBotPositionDetail => POSITION_DETAIL[position]; export const coercePositionGroup = (position: string): SoccerBotPositionGroup => { if (['Goalkeeper', 'Brankář', 'Brankáři', 'Brankári'].includes(position)) { @@ -20,7 +39,9 @@ export const coercePositionGroup = (position: string): SoccerBotPositionGroup => ].includes(position) ) { return SoccerBotPositionGroup.MIDFIELDER; - } else if (['Left Winger', 'Right Winger', 'Centre-Forward', 'Attacker', 'Útočník', 'Útočníci'].includes(position)) { + } else if ( + ['Left Winger', 'Right Winger', 'Centre-Forward', 'Forward', 'Attacker', 'Útočník', 'Útočníci'].includes(position) + ) { return SoccerBotPositionGroup.ATTACKER; } return undefined; diff --git a/src/index.ts b/src/index.ts index 6da61f8..57376da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { SoccerBotSportnetClient, SoccerBotTransfermarktClient } from './clients'; +export { SoccerBotFoot, SoccerBotPositionDetail, SoccerBotPositionGroup } from './shared/interfaces'; export const transfermarkt = new SoccerBotTransfermarktClient(); export const soccerway = new SoccerBotSoccerwayClient(); diff --git a/src/shared/interfaces.ts b/src/shared/interfaces.ts index ad1482c..b4c12a2 100644 --- a/src/shared/interfaces.ts +++ b/src/shared/interfaces.ts @@ -12,6 +12,37 @@ export enum SoccerBotPositionGroup { ATTACKER = 'ATTACKER' } +export enum SoccerBotPositionDetail { + GK = 'GK', + SW = 'SW', + LWB = 'LWB', + LB = 'LB', + LCB = 'LCB', + CB = 'CB', + RCB = 'RCB', + RB = 'RB', + RWB = 'RWB', + LDM = 'LDM', + CDM = 'CDM', + RDM = 'RDM', + LM = 'LM', + LCM = 'LCM', + CM = 'CM', + RCM = 'RCM', + RM = 'RM', + LAM = 'LAM', + CAM = 'CAM', + RAM = 'RAM', + LW = 'LW', + LF = 'LF', + CF = 'CF', + RF = 'RF', + RW = 'RW', + LS = 'LS', + ST = 'ST', + RS = 'RS' +} + export interface SoccerBotResponse { ok: boolean; data?: T; @@ -36,6 +67,7 @@ export interface SoccerBotPlayer { lastName?: string; jerseyNumber?: number; position?: SoccerBotPositionGroup; + positionDetail?: SoccerBotPositionDetail; birthdate?: string; height?: number; weight?: number; diff --git a/test/client/eurofotbal.spec.ts b/test/client/eurofotbal.spec.ts index a4e4d43..c751f16 100644 --- a/test/client/eurofotbal.spec.ts +++ b/test/client/eurofotbal.spec.ts @@ -1,5 +1,6 @@ import { vi } from 'vitest'; import { SoccerBotEurofotbalClient } from '../../src/clients/eurofotbal'; +import { LEAGUE_DATA, LEAGUE_HTML } from '../mocks/eurofotbal/league'; import { TEAM_DATA, TEAM_HTML } from '../mocks/eurofotbal/team'; describe('SoccerBotEurofotbalClient', () => { @@ -9,18 +10,67 @@ describe('SoccerBotEurofotbalClient', () => { client = new SoccerBotEurofotbalClient(); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('leagueUrl', () => { + it('should return correct value', () => { + expect(client.leagueUrl('chance-liga')).toEqual('https://www.eurofotbal.cz/chance-liga/tabulky/'); + }); + + it('should return undefined', () => { + expect(client.leagueUrl(undefined)).toEqual(undefined); + expect(client.leagueUrl(null)).toEqual(undefined); + expect(client.leagueUrl('')).toEqual(undefined); + }); + }); + + describe('teamUrl', () => { + it('should return correct value', () => { + expect(client.teamUrl('cesko/slavia-praha')).toEqual( + 'https://www.eurofotbal.cz/kluby/cesko/slavia-praha/soupiska' + ); + }); + + it('should return undefined', () => { + expect(client.teamUrl(undefined)).toEqual(undefined); + expect(client.teamUrl(null)).toEqual(undefined); + expect(client.teamUrl('')).toEqual(undefined); + }); + }); + + describe('league', () => { + beforeEach(() => { + const handleSpy = vi.spyOn(SoccerBotEurofotbalClient.prototype as any, 'fetchPage'); + handleSpy.mockResolvedValue(LEAGUE_HTML); + }); + + it('should return league', async () => { + const result = await client.league('chance-liga'); + + expect(result).toEqual(LEAGUE_DATA); + if (result.ok) { + expect(result.data).toHaveLength(16); + expect(result.data).toContainEqual({ id: 'cesko/slavia-praha', name: 'Slavia Praha' }); + } + }); + }); + describe('team', () => { beforeEach(() => { const handleSpy = vi.spyOn(SoccerBotEurofotbalClient.prototype as any, 'fetchPage'); - handleSpy.mockImplementation(() => { - return new Promise((resolve) => { - resolve(TEAM_HTML); - }); - }); + handleSpy.mockResolvedValue(TEAM_HTML); }); it('should return team', async () => { - expect(await client.team('cesko/slavia-praha')).toEqual(TEAM_DATA); + const result = await client.team('cesko/slavia-praha'); + + expect(result).toEqual(TEAM_DATA); + if (result.ok) { + expect(result.data).toHaveLength(33); + expect(result.data).toContainEqual(expect.objectContaining({ id: 'ondrej-kolar-32017', name: 'Ondřej Kolář' })); + } }); }); }); diff --git a/test/client/soccerway.spec.ts b/test/client/soccerway.spec.ts index eb27521..f736ff4 100644 --- a/test/client/soccerway.spec.ts +++ b/test/client/soccerway.spec.ts @@ -11,31 +11,29 @@ describe('SoccerBotSoccerwayClient', () => { client = new SoccerBotSoccerwayClient(5); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + describe('leagueUrl', () => { it('should return correct value', () => { - expect(client.leagueUrl('r59164', '20202021')).toEqual( - 'https://int.soccerway.com/national/country-slug/team-slug/20202021/regular-season/r59164/tables/' + expect(client.leagueUrl('czech-republic/chance-liga/standings/bNFMkskm')).toEqual( + 'https://www.soccerway.com/czech-republic/chance-liga/standings/bNFMkskm/standings/overall/' ); }); it('should return undefined', () => { - expect(client.leagueUrl(undefined, '20202021')).toEqual(undefined); - expect(client.leagueUrl('r59164', undefined)).toEqual(undefined); - expect(client.leagueUrl(undefined, undefined)).toEqual(undefined); - - expect(client.leagueUrl(null, '20202021')).toEqual(undefined); - expect(client.leagueUrl('r59164', null)).toEqual(undefined); - expect(client.leagueUrl(null, null)).toEqual(undefined); - - expect(client.leagueUrl('', '20202021')).toEqual(undefined); - expect(client.leagueUrl('r59164', '')).toEqual(undefined); - expect(client.leagueUrl('', '')).toEqual(undefined); + expect(client.leagueUrl(undefined)).toEqual(undefined); + expect(client.leagueUrl(null)).toEqual(undefined); + expect(client.leagueUrl('')).toEqual(undefined); }); }); describe('teamUrl', () => { it('should return correct value', () => { - expect(client.teamUrl('533')).toEqual('https://int.soccerway.com/teams/country-slug/team-slug/533/squad/'); + expect(client.teamUrl('slavia-prague/viXGgnyB')).toEqual( + 'https://www.soccerway.com/team/slavia-prague/viXGgnyB/squad/' + ); }); it('should return undefined', () => { @@ -47,7 +45,9 @@ describe('SoccerBotSoccerwayClient', () => { describe('playerUrl', () => { it('should return correct value', () => { - expect(client.playerUrl('193498')).toEqual('https://int.soccerway.com/players/player-slug/193498/'); + expect(client.playerUrl('kolar-ondrej/xfBGcS1U')).toEqual( + 'https://www.soccerway.com/player/kolar-ondrej/xfBGcS1U/' + ); }); it('should return undefined', () => { @@ -68,11 +68,21 @@ describe('SoccerBotSoccerwayClient', () => { }); it('should return league', async () => { - expect(await client.league('r59164', '20202021')).toEqual(LEAGUE_DATA); + const result = await client.league('czech-republic/chance-liga/standings/bNFMkskm'); + + expect(result).toEqual(LEAGUE_DATA); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data).toHaveLength(16); + expect(new Set(result.data.map(({ id }) => id)).size).toBe(16); + expect(result.data).toContainEqual({ id: 'slavia-prague/viXGgnyB', name: 'Slavia Prague' }); + } }); }); describe('team', () => { + let handlePlayerSpy: ReturnType; + beforeEach(() => { const handleSpy = vi.spyOn(SoccerBotSoccerwayClient.prototype as any, 'fetchPage'); handleSpy.mockImplementation(() => { @@ -81,16 +91,24 @@ describe('SoccerBotSoccerwayClient', () => { }); }); - const handlePlayerSpy = vi.spyOn(SoccerBotSoccerwayClient.prototype as any, 'player'); - handlePlayerSpy.mockImplementation(() => { + handlePlayerSpy = vi.spyOn(SoccerBotSoccerwayClient.prototype as any, 'player'); + handlePlayerSpy.mockImplementation((id: string) => { return new Promise((resolve) => { - resolve(PLAYER_DATA); + resolve({ + ok: true, + data: { id } + }); }); }); }); it('should return team', async () => { - expect(await client.team('533')).toEqual(TEAM_DATA); + expect(await client.team('slavia-prague/viXGgnyB')).toEqual(TEAM_DATA); + expect(handlePlayerSpy).toHaveBeenCalledTimes(48); + expect(handlePlayerSpy).toHaveBeenCalledWith('kolar-ondrej/xfBGcS1U'); + if (TEAM_DATA.ok) { + expect(handlePlayerSpy.mock.calls.map(([id]) => id)).toEqual(TEAM_DATA.data.map(({ id }) => id)); + } }); }); @@ -105,7 +123,7 @@ describe('SoccerBotSoccerwayClient', () => { }); it('should return player', async () => { - expect(await client.player('193498')).toEqual(PLAYER_DATA); + expect(await client.player('kolar-ondrej/xfBGcS1U')).toEqual(PLAYER_DATA); }); }); }); diff --git a/test/client/sportnet.spec.ts b/test/client/sportnet.spec.ts index d09b128..c68ebb7 100644 --- a/test/client/sportnet.spec.ts +++ b/test/client/sportnet.spec.ts @@ -12,6 +12,10 @@ describe('SoccerBotSportnetClient', () => { client = new SoccerBotSportnetClient(5); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + describe('leagueUrl', () => { it('should return correct value', () => { expect(client.leagueUrl('sfz/s/3528')).toEqual('https://sportnet.sme.sk/futbalnet/z/sfz/s/3528/tabulky/'); @@ -51,9 +55,11 @@ describe('SoccerBotSportnetClient', () => { }); describe('league', () => { + let fetchPageSpy: ReturnType; + beforeEach(() => { - const handleSpy = vi.spyOn(SoccerBotSportnetClient.prototype as any, 'fetchPage'); - handleSpy.mockImplementation(() => { + fetchPageSpy = vi.spyOn(SoccerBotSportnetClient.prototype as any, 'fetchPage'); + fetchPageSpy.mockImplementation(() => { return new Promise((resolve) => { resolve(LEAGUE_HTML); }); @@ -63,17 +69,37 @@ describe('SoccerBotSportnetClient', () => { it('should return league', async () => { expect(await client.league('sfz/s/3528')).toEqual(LEAGUE_DATA); }); + + it('should skip unrelated table rows', async () => { + fetchPageSpy.mockResolvedValue(` + + + + + +
Unrelated table row
Example FC
+ `); + + expect(await client.league('sfz/s/monacobet-liga')).toEqual({ + ok: true, + data: [{ id: 'example-fc/tim/dospeli-m-a', name: 'Example FC' }] + }); + }); }); describe('team', () => { + let fetchPageSpy: ReturnType; + let playerSpy: ReturnType; + beforeEach(() => { - vi.spyOn(SoccerBotSportnetClient.prototype as any, 'player').mockImplementation(() => { + playerSpy = vi.spyOn(SoccerBotSportnetClient.prototype as any, 'player').mockImplementation(() => { return new Promise((resolve) => { resolve(PLAYER_DATA); }); }); - vi.spyOn(SoccerBotSportnetClient.prototype as any, 'fetchPage') + fetchPageSpy = vi + .spyOn(SoccerBotSportnetClient.prototype as any, 'fetchPage') .mockImplementationOnce(() => { return new Promise((resolve) => { resolve(TEAM_HTML); @@ -102,6 +128,14 @@ describe('SoccerBotSportnetClient', () => { response.data = response.data.map((item) => ({ ...item, position: SoccerBotPositionGroup.GOALKEEPER })); expect(response).toEqual(data); }); + + it('should return an empty team without player lookups', async () => { + fetchPageSpy.mockReset().mockResolvedValue('

Nepodarilo sa získať súpisku tímu!

'); + + expect(await client.team('fk-inter-bratislava-1/tim/dospeli-m-a')).toEqual({ ok: true, data: [] }); + expect(fetchPageSpy).toHaveBeenCalledTimes(1); + expect(playerSpy).not.toHaveBeenCalled(); + }); }); describe('player', () => { diff --git a/test/helpers/country.spec.ts b/test/helpers/country.spec.ts index c3d22d8..b3f1677 100644 --- a/test/helpers/country.spec.ts +++ b/test/helpers/country.spec.ts @@ -26,6 +26,11 @@ describe('coerceCountry', () => { code2: SoccerBotCountryCode2.AF, code3: SoccerBotCountryCode3.AFG }); + expect(coerceCountry('Czech Republic', SoccerBotProvider.SOCCERWAY)).toEqual({ + databaseName: 'Czech Republic', + code2: SoccerBotCountryCode2.CZ, + code3: SoccerBotCountryCode3.CZE + }); }); it('should return undefined', () => { diff --git a/test/helpers/date.spec.ts b/test/helpers/date.spec.ts index e646013..846753a 100644 --- a/test/helpers/date.spec.ts +++ b/test/helpers/date.spec.ts @@ -19,6 +19,7 @@ describe('coerceDate', () => { describe('soccerway', () => { it('should return correct value', () => { expect(coerceDate('4 March 1993', SoccerBotProvider.SOCCERWAY)).toEqual('1993-03-04'); + expect(coerceDate('Age: 31 (17.10.1994)', SoccerBotProvider.SOCCERWAY)).toEqual('1994-10-17'); }); it('should return undefined', () => { diff --git a/test/helpers/position.spec.ts b/test/helpers/position.spec.ts index 7fe1794..bc6bf3b 100644 --- a/test/helpers/position.spec.ts +++ b/test/helpers/position.spec.ts @@ -1,5 +1,41 @@ -import { coercePositionGroup } from '../../src/helpers/position'; -import { SoccerBotPositionGroup } from '../../src/shared/interfaces'; +import { SoccerBotFoot, SoccerBotPositionDetail, SoccerBotPositionGroup } from '../../src'; +import { coercePositionDetail, coercePositionGroup } from '../../src/helpers/position'; + +describe('coercePositionDetail', () => { + it.each([ + ['Goalkeeper', SoccerBotPositionDetail.GK], + ['Sweeper', SoccerBotPositionDetail.SW], + ['Centre-Back', SoccerBotPositionDetail.CB], + ['Left-Back', SoccerBotPositionDetail.LB], + ['Right-Back', SoccerBotPositionDetail.RB], + ['Defensive Midfield', SoccerBotPositionDetail.CDM], + ['Central Midfield', SoccerBotPositionDetail.CM], + ['Right Midfield', SoccerBotPositionDetail.RM], + ['Left Midfield', SoccerBotPositionDetail.LM], + ['Attacking Midfield', SoccerBotPositionDetail.CAM], + ['Left Winger', SoccerBotPositionDetail.LW], + ['Right Winger', SoccerBotPositionDetail.RW], + ['Second Striker', SoccerBotPositionDetail.CF], + ['Centre-Forward', SoccerBotPositionDetail.ST] + ])('should map %s to %s', (position, expected) => { + expect(coercePositionDetail(position)).toEqual(expected); + }); + + it.each(['Defender', 'Midfielder', 'Forward', 'Attacker', 'xxx', '', null, undefined])( + 'should return undefined for %s', + (position) => { + expect(coercePositionDetail(position)).toEqual(undefined); + } + ); + + it('should export the enum from the package root', () => { + expect(SoccerBotFoot.RIGHT).toEqual('RIGHT'); + expect(SoccerBotPositionDetail.GK).toEqual('GK'); + expect(SoccerBotPositionDetail.RCB).toEqual('RCB'); + expect(SoccerBotPositionDetail.ST).toEqual('ST'); + expect(SoccerBotPositionGroup.GOALKEEPER).toEqual('GOALKEEPER'); + }); +}); describe('coercePositionGroup', () => { it('should return goalkeeper', () => { @@ -34,6 +70,7 @@ describe('coercePositionGroup', () => { expect(coercePositionGroup('Left Winger')).toEqual(SoccerBotPositionGroup.ATTACKER); expect(coercePositionGroup('Right Winger')).toEqual(SoccerBotPositionGroup.ATTACKER); expect(coercePositionGroup('Centre-Forward')).toEqual(SoccerBotPositionGroup.ATTACKER); + expect(coercePositionGroup('Forward')).toEqual(SoccerBotPositionGroup.ATTACKER); expect(coercePositionGroup('Attacker')).toEqual(SoccerBotPositionGroup.ATTACKER); expect(coercePositionGroup('Útočník')).toEqual(SoccerBotPositionGroup.ATTACKER); expect(coercePositionGroup('Útočníci')).toEqual(SoccerBotPositionGroup.ATTACKER); diff --git a/test/mocks/eurofotbal/league.ts b/test/mocks/eurofotbal/league.ts index 4422d28..d9828cc 100644 --- a/test/mocks/eurofotbal/league.ts +++ b/test/mocks/eurofotbal/league.ts @@ -1,7 +1,6 @@ import { SoccerBotResponse, SoccerBotTeam } from '../../../src/shared/interfaces'; -export const LEAGUE_HTML = ` - +export const LEAGUE_HTML = ` @@ -9,8 +8,8 @@ export const LEAGUE_HTML = ` - FORTUNA:LIGA - Fotbal Česko - FORTUNA:LIGA: zprávy, výsledky, statistiky - + Česko - Chance Liga - tabulky + - - - - - - - + + + + + + + @@ -75,9 +65,11 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
-
+
+
+
@@ -132,7 +124,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=