diff --git a/.github/workflows/pull-request-validation.yml b/.github/workflows/pull-request-validation.yml index ff0491ca2f..87574147f0 100644 --- a/.github/workflows/pull-request-validation.yml +++ b/.github/workflows/pull-request-validation.yml @@ -145,6 +145,7 @@ jobs: --forceExit \ --logHeapUsage \ --runInBand \ + --silent \ --testPathPattern=/packages/\ timeout-minutes: 10 @@ -154,7 +155,7 @@ jobs: ls -laR . mv lcov.info lcov-unit.info - mv result.trx result-unit.trx + # mv result.trx result-unit.trx working-directory: ./coverage - if: always() @@ -164,7 +165,7 @@ jobs: name: test-result-unit path: | ./coverage/lcov-*.info - ./coverage/result-*.trx + # ./coverage/result-*.trx type-definitions-test: name: Type definitions test @@ -255,7 +256,8 @@ jobs: --forceExit \ --logHeapUsage \ --runInBand \ - --shard=${{ format('{0}/{1}', matrix.shard-index, matrix.shard-count) }} + --shard=${{ format('{0}/{1}', matrix.shard-index, matrix.shard-count) }} \ + --silent timeout-minutes: 10 - if: always() @@ -268,7 +270,7 @@ jobs: ls -laR . mv lcov.info lcov-${{ matrix.shard-index }}.info - mv result.trx result-${{ matrix.shard-index }}.trx + # mv result.trx result-${{ matrix.shard-index }}.trx working-directory: ./coverage - if: always() @@ -278,7 +280,7 @@ jobs: name: test-result-html-${{ matrix.shard-index }} path: | ./coverage/lcov-*.info - ./coverage/result-*.trx + # ./coverage/result-*.trx - if: failure() name: Upload test snapshot diffs diff --git a/ACHIEVEMENTS.md b/ACHIEVEMENTS.md index cbb79ff02b..4b2989182d 100644 --- a/ACHIEVEMENTS.md +++ b/ACHIEVEMENTS.md @@ -4,7 +4,7 @@ A curated list of major achievements by the Web Chat team. This document celebra - - - - - - - - - - - - -
- - - diff --git a/__tests__/setup/web/mockWebSpeech.js b/__tests__/setup/web/mockWebSpeech.js deleted file mode 100644 index 568590edf0..0000000000 --- a/__tests__/setup/web/mockWebSpeech.js +++ /dev/null @@ -1,369 +0,0 @@ -const { defineEventAttribute } = (EventTarget = window.EventTargetShim); -const NULL_FN = () => 0; - -function createSpeechRecognitionResults(isFinal, transcript) { - const results = [ - [ - { - confidence: 0.9, - transcript - } - ] - ]; - - results[0].isFinal = isFinal; - - return results; -} - -function createProducerConsumer() { - const consumers = []; - const jobs = []; - - return { - cancel() { - jobs.splice(0); - }, - consume(fn, context) { - consumers.push({ fn, context }); - jobs.length && consumers.shift().fn(...jobs.shift()); - }, - hasConsumer() { - return !!consumers.length && consumers[0].context; - }, - hasJob() { - return !!jobs.length; - }, - peek() { - return jobs[0]; - }, - produce(...args) { - jobs.push(args); - consumers.length && consumers.shift().fn(...jobs.shift()); - } - }; -} - -const speechRecognitionBroker = createProducerConsumer(); -const speechSynthesisBroker = createProducerConsumer(); - -class SpeechRecognition extends EventTarget { - constructor() { - super(); - - this.grammars = null; - this.lang = 'en-US'; - this.continuous = false; - this.interimResults = false; - this.maxAlternatives = 1; - this.serviceURI = 'mock://microsoft.com/web-speech-recognition'; - - this.abort = this.stop = NULL_FN; - } - - start() { - speechRecognitionBroker.consume((scenario, ...args) => { - if (!this[scenario]) { - throw new Error(`Cannot find speech scenario named "${scenario}" in mockWebSpeech.js`); - } else { - this[scenario](...args); - } - }, this); - } - - microphoneMuted() { - this.abort = this.stop = NULL_FN; - - this.dispatchEvent({ type: 'start' }); - this.dispatchEvent({ type: 'audiostart' }); - this.dispatchEvent({ type: 'audioend' }); - this.dispatchEvent({ type: 'error', error: 'no-speech' }); - this.dispatchEvent({ type: 'end' }); - } - - birdTweet() { - this.abort = this.stop = NULL_FN; - - this.dispatchEvent({ type: 'start' }); - this.dispatchEvent({ type: 'audiostart' }); - this.dispatchEvent({ type: 'soundstart' }); - this.dispatchEvent({ type: 'soundend' }); - this.dispatchEvent({ type: 'audioend' }); - this.dispatchEvent({ type: 'end' }); - } - - unrecognizableSpeech() { - this.abort = this.stop = NULL_FN; - - this.dispatchEvent({ type: 'start' }); - this.dispatchEvent({ type: 'audiostart' }); - this.dispatchEvent({ type: 'soundstart' }); - this.dispatchEvent({ type: 'speechstart' }); - this.dispatchEvent({ type: 'speechend' }); - this.dispatchEvent({ type: 'soundend' }); - this.dispatchEvent({ type: 'audioend' }); - this.dispatchEvent({ type: 'end' }); - } - - airplaneMode() { - this.abort = this.stop = NULL_FN; - - this.dispatchEvent({ type: 'start' }); - this.dispatchEvent({ type: 'audiostart' }); - this.dispatchEvent({ type: 'audioend' }); - this.dispatchEvent({ type: 'error', error: 'network' }); - this.dispatchEvent({ type: 'end' }); - } - - accessDenied() { - this.abort = this.stop = NULL_FN; - - this.dispatchEvent({ type: 'error', error: 'not-allowed' }); - this.dispatchEvent({ type: 'end' }); - } - - abortAfterAudioStart() { - this.abort = () => { - this.dispatchEvent({ type: 'audioend' }); - this.dispatchEvent({ type: 'error', error: 'aborted' }); - this.dispatchEvent({ type: 'end' }); - }; - - this.stop = NULL_FN; - - this.dispatchEvent({ type: 'start' }); - this.dispatchEvent({ type: 'audiostart' }); - } - - recognize(transcript) { - this.abort = this.stop = NULL_FN; - - this.dispatchEvent({ type: 'start' }); - this.dispatchEvent({ type: 'audiostart' }); - this.dispatchEvent({ type: 'soundstart' }); - this.dispatchEvent({ type: 'speechstart' }); - - this.interimResults && - this.dispatchEvent({ type: 'result', results: createSpeechRecognitionResults(false, transcript) }); - - this.dispatchEvent({ type: 'speechend' }); - this.dispatchEvent({ type: 'soundend' }); - this.dispatchEvent({ type: 'audioend' }); - - this.dispatchEvent({ type: 'result', results: createSpeechRecognitionResults(true, transcript) }); - this.dispatchEvent({ type: 'end' }); - } - - recognizing(transcript) { - this.abort = this.stop = NULL_FN; - - this.dispatchEvent({ type: 'start' }); - this.dispatchEvent({ type: 'audiostart' }); - this.dispatchEvent({ type: 'soundstart' }); - this.dispatchEvent({ type: 'speechstart' }); - - this.interimResults && - this.dispatchEvent({ type: 'result', results: createSpeechRecognitionResults(false, transcript) }); - } - - recognizeButAborted(transcript) { - this.abort = () => { - this.dispatchEvent({ type: 'speechend' }); - this.dispatchEvent({ type: 'soundend' }); - this.dispatchEvent({ type: 'audioend' }); - this.dispatchEvent({ type: 'error', error: 'aborted' }); - this.dispatchEvent({ type: 'end' }); - }; - - this.stop = NULL_FN; - - this.dispatchEvent({ type: 'start' }); - this.dispatchEvent({ type: 'audiostart' }); - this.dispatchEvent({ type: 'soundstart' }); - this.dispatchEvent({ type: 'speechstart' }); - this.interimResults && - this.dispatchEvent({ type: 'result', results: createSpeechRecognitionResults(false, transcript) }); - } - - recognizeButNotConfident(transcript) { - this.abort = this.stop = NULL_FN; - - this.dispatchEvent({ type: 'start' }); - this.dispatchEvent({ type: 'audiostart' }); - this.dispatchEvent({ type: 'soundstart' }); - this.dispatchEvent({ type: 'speechstart' }); - this.interimResults && - this.dispatchEvent({ type: 'result', results: createSpeechRecognitionResults(false, transcript) }); - this.dispatchEvent({ type: 'speechend' }); - this.dispatchEvent({ type: 'soundend' }); - this.dispatchEvent({ type: 'audioend' }); - this.dispatchEvent({ type: 'result', results: createSpeechRecognitionResults(false, transcript) }); - this.dispatchEvent({ type: 'end' }); - } -} - -[ - 'audiostart', - 'audioend', - 'end', - 'error', - 'nomatch', - 'result', - 'soundstart', - 'soundend', - 'speechstart', - 'speechend', - 'start' -].forEach(name => defineEventAttribute(SpeechRecognition.prototype, name)); - -class SpeechGrammarList { - addFromString() { - throw new Error('Not implemented'); - } - - addFromURI() { - throw new Error('Not implemented'); - } -} - -const SPEECH_SYNTHESIS_VOICES = [ - { - default: true, - lang: 'en-US', - localService: true, - name: 'Mock Voice (en-US)', - voiceURI: 'mock://web-speech/voice/en-US' - }, - { - default: false, - lang: 'zh-YUE', - localService: true, - name: 'Mock Voice (zh-YUE)', - voiceURI: 'mock://web-speech/voice/zh-YUE' - } -]; - -class SpeechSynthesis extends EventTarget { - constructor() { - super(); - - this.ignoreFirstUtteranceIfEmpty = true; - } - - getVoices() { - return SPEECH_SYNTHESIS_VOICES; - } - - cancel() { - speechSynthesisBroker.cancel(); - } - - pause() { - throw new Error('pause is not implemented.'); - } - - resume() { - throw new Error('resume is not implemented.'); - } - - speak(utterance) { - // We prime the speech engine by sending an empty utterance on start. - // We should ignore the first utterance if it is empty. - if (!this.ignoreFirstUtteranceIfEmpty || utterance.text) { - speechSynthesisBroker.produce(utterance); - } - - this.ignoreFirstUtteranceIfEmpty = false; - } -} - -defineEventAttribute(SpeechSynthesis.prototype, 'voiceschanged'); - -class SpeechSynthesisUtterance extends EventTarget { - constructor(text) { - super(); - - this.lang = SPEECH_SYNTHESIS_VOICES[0].lang; - this.pitch = 1; - this.rate = 1; - this.text = text; - this.voice = SPEECH_SYNTHESIS_VOICES[0]; - this.volume = 1; - } -} - -['boundary', 'end', 'error', 'mark', 'pause', 'resume', 'start'].forEach(name => - defineEventAttribute(SpeechSynthesisUtterance.prototype, name) -); - -window.WebSpeechMock = { - mockEndSynthesize() { - return new Promise(resolve => { - speechSynthesisBroker.consume(utterance => { - utterance.dispatchEvent({ type: 'end' }); - - const { lang, pitch, rate, text, voice, volume } = utterance; - - resolve({ lang, pitch, rate, text, voice, volume }); - }); - }); - }, - - mockErrorSynthesize(error = 'artificial-error') { - return new Promise(resolve => { - speechSynthesisBroker.consume(utterance => { - utterance.dispatchEvent({ error, type: 'error' }); - - const { lang, pitch, rate, text, voice, volume } = utterance; - - resolve({ lang, pitch, rate, text, voice, volume }); - }); - }); - }, - - mockRecognize(...args) { - speechRecognitionBroker.produce(...args); - }, - - mockStartSynthesize() { - const [utterance] = speechSynthesisBroker.peek() || []; - - if (!utterance) { - throw new Error('No utterance pending synthesize.'); - } - - utterance.dispatchEvent({ type: 'start' }); - - const { lang, pitch, rate, text, voice, volume } = utterance; - - return { lang, pitch, rate, text, voice, volume }; - }, - - speechRecognitionStartCalled() { - const context = speechRecognitionBroker.hasConsumer(); - - if (context) { - const { continuous, grammars, interimResults, lang, maxAlternatives, serviceURI } = context; - - return { - continuous, - grammars, - interimResults, - lang, - maxAlternatives, - serviceURI - }; - } else { - return false; - } - }, - - speechSynthesisUtterancePended() { - return speechSynthesisBroker.hasJob(); - }, - - SpeechGrammarList, - SpeechRecognition, - speechSynthesis: new SpeechSynthesis(), - SpeechSynthesisUtterance -}; diff --git a/__tests__/setup/jestNodeEnvironmentWithTimezone.js b/__tests__/unit.setup/JestNodeEnvironmentWithTimeZone.js similarity index 87% rename from __tests__/setup/jestNodeEnvironmentWithTimezone.js rename to __tests__/unit.setup/JestNodeEnvironmentWithTimeZone.js index 47ab457b9f..b70ed0e56e 100644 --- a/__tests__/setup/jestNodeEnvironmentWithTimezone.js +++ b/__tests__/unit.setup/JestNodeEnvironmentWithTimeZone.js @@ -1,3 +1,5 @@ +/* eslint-env node */ + // Adopted from https://stackoverflow.com/questions/56261381/how-do-i-set-a-timezone-in-my-jest-config. const { TestEnvironment } = require('jest-environment-node'); @@ -5,7 +7,7 @@ const { TestEnvironment } = require('jest-environment-node'); * Timezone-aware Jest environment. Supports `@timezone` JSDoc * pragma within test suites to set timezone. */ -module.exports = class TimezoneAwareNodeEnvironment extends TestEnvironment { +module.exports = class JestNodeEnvironmentWithTimeZone extends TestEnvironment { constructor(config, context) { // Allow test suites to change timezone, even if TZ is passed in a script. // Falls back to existing TZ environment variable or UTC if no timezone is specified. diff --git a/jest.legacy.config.js b/__tests__/unit.setup/jest.config.js similarity index 62% rename from jest.legacy.config.js rename to __tests__/unit.setup/jest.config.js index bdc9bc0a90..4ce7a63500 100644 --- a/jest.legacy.config.js +++ b/__tests__/unit.setup/jest.config.js @@ -1,15 +1,16 @@ const { defaults } = require('jest-config'); const TRANSFORM_IGNORE_PACKAGES = [ - 'botframework-webchat-api', - 'botframework-webchat-component', - 'botframework-webchat-core', - 'botframework-webchat', + // General + 'merge-refs', + 'mime', + 'uuid', + + // Related to micromark 'character-entities', 'decode-named-character-reference', 'mdast-util-from-markdown', 'mdast-util-to-string', - 'merge-refs', 'micromark-core-commonmark', 'micromark-extension-gfm', 'micromark-extension-gfm-autolink-literal', @@ -37,61 +38,66 @@ const TRANSFORM_IGNORE_PACKAGES = [ 'micromark-util-sanitize-uri', 'micromark-util-subtokenize', 'micromark', - 'mime', 'unist-util-stringify-position', - 'uuid', - // Related to Speech SDK. + // Related to Speech SDK 'microsoft-cognitiveservices-speech-sdk', - // Related to Adaptive Cards. + // Related to Adaptive Cards 'dom7', 'ssr-window', 'swiper' ]; module.exports = { - displayName: { color: 'yellow', name: 'legacy' }, + displayName: { color: 'yellow', name: 'unit' }, globals: { npm_package_version: '0.0.0-0.jest' }, moduleDirectories: ['node_modules', 'packages'], moduleFileExtensions: ['js', 'jsx', 'mjs', 'ts', 'tsx'], - rootDir: './', + rootDir: '../../packages/', setupFilesAfterEnv: [ - '/__tests__/setup/setupDotEnv.js', - '/__tests__/setup/setupGlobalAgent.js', - '/__tests__/setup/preSetupTestFramework.js', - '/__tests__/setup/setupCryptoGetRandomValues.js', - '/__tests__/setup/setupCryptoRandomUUID.js', - '/__tests__/setup/setupImageSnapshot.js', - '/__tests__/setup/setupTestNightly.js', - '/__tests__/setup/setupTimeout.js' + '/../__tests__/unit.setup/setupFiles/setupCrypto.js', + '/../__tests__/unit.setup/setupFiles/setupTestNightly.js', + '/../__tests__/unit.setup/setupFiles/setupTimeout.js' ], - testMatch: ['**/__tests__/**/*.?([mc])[jt]s?(x)', '**/?(*.)+(spec|test).?([mc])[jt]s?(x)'], + testMatch: ['**/?(*.)+(spec|test).?([mc])[jt]s?(x)'], testPathIgnorePatterns: [ '/dist/', '/lib/', '/node_modules/', '/static/', - '/__tests__/assets/', - '/__tests__/html2/', // Will be tested by jest.html2.config.js. - '/__tests__/setup/', - '/packages/bundle/__tests__/types/__typescript__/', - '/packages/core/__tests__/types/__typescript__/', - '/packages/directlinespeech/__tests__/utilities/', - '/packages/playground/', - '/samples/' + '/bundle/__tests__/types/__typescript__/', + '/core/__tests__/types/__typescript__/', + '/directlinespeech/__tests__/utilities/', + '/playground/' ], transform: { - '\\.m?[jt]sx?$': '/babel-jest-config.js' + '\\.m?[jt]sx?$': [ + 'babel-jest', + { + presets: [ + [ + '@babel/preset-env', + { + modules: 'commonjs' + } + ], + '@babel/preset-typescript', + '@babel/preset-react' + ] + } + ] }, transformIgnorePatterns: [ // jest-environment-jsdom import packages as browser. // Packages, such as "uuid", export itself for browser as ES5 + ESM. // Since jest@28 cannot consume ESM yet, we need to transpile these packages. `/node_modules/(?!(${TRANSFORM_IGNORE_PACKAGES.join('|')})/)`, - '/packages/(?:test/)?\\w+/(?:lib/|dist/.+?\\.js$|\\w+\\.js)', - ...defaults.transformIgnorePatterns.filter(pattern => pattern !== '/node_modules/') + ...defaults.transformIgnorePatterns.filter(pattern => pattern !== '/node_modules/'), + + // Do not transform anything under /test/*/(dist|lib). + '/packages/(?:test/)?\\w+/(?:lib/|dist/.+?\\.js$|\\w+\\.js)' ] }; diff --git a/__tests__/unit.setup/setupFiles/setupCrypto.js b/__tests__/unit.setup/setupFiles/setupCrypto.js new file mode 100644 index 0000000000..a0da8a0237 --- /dev/null +++ b/__tests__/unit.setup/setupFiles/setupCrypto.js @@ -0,0 +1,23 @@ +/* eslint-env node */ + +import { randomFillSync } from 'crypto'; +import { v4 } from 'uuid'; + +// When microsoft-cognitiveservices-speech-sdk is loaded, it call "uuid" package to create a new GUID. +// "uuid" package requires crypto.getRandomValues(). +if (!global.crypto?.getRandomValues) { + global.crypto = { + ...global.crypto, + getRandomValues: randomFillSync + }; +} + +// In browser, only works in secure context. +if (!global.crypto?.randomUUID) { + global.crypto = { + ...global.crypto, + randomUUID() { + return v4(); + } + }; +} diff --git a/__tests__/unit.setup/setupFiles/setupTestNightly.js b/__tests__/unit.setup/setupFiles/setupTestNightly.js new file mode 100644 index 0000000000..b4fb02203d --- /dev/null +++ b/__tests__/unit.setup/setupFiles/setupTestNightly.js @@ -0,0 +1,3 @@ +/* eslint-env node */ + +test.nightly = process.env.CI_PULL_REQUEST ? test.skip.bind(test) : test; diff --git a/__tests__/unit.setup/setupFiles/setupTimeout.js b/__tests__/unit.setup/setupFiles/setupTimeout.js new file mode 100644 index 0000000000..b5062b8542 --- /dev/null +++ b/__tests__/unit.setup/setupFiles/setupTimeout.js @@ -0,0 +1 @@ +jest.setTimeout(10000); diff --git a/babel-jest-config.js b/babel-jest-config.js deleted file mode 100644 index c6b62b0df7..0000000000 --- a/babel-jest-config.js +++ /dev/null @@ -1,15 +0,0 @@ -const { - default: { createTransformer } -} = require('babel-jest'); -const { join } = require('path'); -const { readFileSync } = require('fs'); - -const stringifiedBabelOptions = readFileSync(join(__dirname, 'babel.config.json'), 'utf8'); -const babelOptions = JSON.parse(stringifiedBabelOptions); -const transformer = createTransformer(babelOptions); - -// Jest is supposed to use babel-jest to consume the .babelrc file in the root of the project, -// but for some reason it can't seem to locate the file, so we must manually load the .babelrc -// file in memory and create a transformer for it. - -module.exports = transformer; diff --git a/babel.config.json b/babel.config.json deleted file mode 100644 index 3c806e6cd2..0000000000 --- a/babel.config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "plugins": [ - "@babel/plugin-transform-runtime" - ], - "presets": [ - [ - "@babel/preset-env", - { - "modules": "commonjs" - } - ], - "@babel/preset-typescript", - "@babel/preset-react" - ] -} diff --git a/docker-compose-wsl2.yml b/docker-compose-wsl2.yml index 19d6b6e2a5..b01dd216e7 100644 --- a/docker-compose-wsl2.yml +++ b/docker-compose-wsl2.yml @@ -1,5 +1,3 @@ -version: '3' - services: # On Windows, run with COMPOSE_CONVERT_WINDOWS_PATHS=1 @@ -7,7 +5,6 @@ services: image: selenium/node-chrome:141.0 depends_on: - selenium-hub - - webchat - webchat2 environment: SE_EVENT_BUS_HOST: selenium-hub @@ -17,7 +14,7 @@ services: SE_NODE_SESSION_TIMEOUT: '300' shm_size: '2.5gb' volumes: - - ./__tests__/setup/local/:/home/seluser/Downloads + - ./__tests__/downloads/:/home/seluser/Downloads selenium-hub: image: selenium/hub:4.36.0 @@ -27,14 +24,6 @@ services: ports: - '4444:4444' - webchat: - build: - context: ./ - dockerfile: testharness.dockerfile - ports: - - '5080:80' - stop_grace_period: 0s - webchat2: build: context: ./ diff --git a/docs/LOCALIZATION.md b/docs/LOCALIZATION.md index caea3a51b2..fcb770ed61 100644 --- a/docs/LOCALIZATION.md +++ b/docs/LOCALIZATION.md @@ -1,6 +1,109 @@ # Localization -Beginning in Web Chat 4.8, this project shifted from community-provided localization to supporting most languages through a dedicated Microsoft team. As of 2020-02-14, 44 languages are officially maintained by Microsoft. Other languages will continue to be community-supported and -driven. +Web Chat includes built-in UI strings for 47 languages. Additionally, you can: + +- Contribute translations for languages not currently supported +- Customize the built-in localization strings to fit your needs + +## Official translations + +The following languages are officially supported by Microsoft: + +| Language code | Language name | +| ------------- | ----------------------------------------- | +| `ar-SA` | Arabic (Saudi Arabia) | +| `bg-BG` | Bulgarian (Bulgaria) | +| `ca-ES` | Catalan (Spain) | +| `cs-CZ` | Czech (Czech Republic) | +| `da-DK` | Danish (Denmark) | +| `de-DE` | German (Germany) | +| `el-GR` | Greek (Greece) | +| `en-US` | English (United States) | +| `es-ES` | Spanish (Spain) | +| `et-EE` | Estonian (Estonia) | +| `eu-ES` | Basque (Spain) | +| `fi-FI` | Finnish (Finland) | +| `fr-FR` | French (France) | +| `gl-ES` | Galician (Spain) | +| `he-IL` | Hebrew (Israel) | +| `hi-IN` | Hindi (India) | +| `hr-HR` | Croatian (Croatia) | +| `hu-HU` | Hungarian (Hungary) | +| `id-ID` | Indonesian (Indonesia) | +| `it-IT` | Italian (Italy) | +| `ja-JP` | Japanese (Japan) | +| `kk-KZ` | Kazakh (Kazakhstan) | +| `ko-KR` | Korean (Korea) | +| `lt-LT` | Lithuanian (Lithuania) | +| `lv-LV` | Latvian (Latvia) | +| `ms-MY` | Malay (Malaysia) | +| `nb-NO` | Norwegian Bokmål (Norway) | +| `nl-NL` | Dutch (Netherlands) | +| `pl-PL` | Polish (Poland) | +| `pt-BR` | Portuguese (Brazil) | +| `pt-PT` | Portuguese (Portugal) | +| `ro-RO` | Romanian (Romania) | +| `ru-RU` | Russian (Russia) | +| `sk-SK` | Slovak (Slovakia) | +| `sl-SI` | Slovenian (Slovenia) | +| `sl-SL` | Slovenian (Slovenia) | +| `sr-Cyrl-CS` | Serbian (Cyrillic, Serbia and Montenegro) | +| `sr-Cyrl` | Serbian (Cyrillic) | +| `sr-Latn-CS` | Serbian (Latin, Serbia and Montenegro) | +| `sr-Latn` | Serbian (Latin) | +| `sv-SE` | Swedish (Sweden) | +| `th-TH` | Thai (Thailand) | +| `tr-TR` | Turkish (Turkey) | +| `uk-UA` | Ukrainian (Ukraine) | +| `vi-VN` | Vietnamese (Vietnam) | +| `zh-CN` | Chinese (Simplified, China) | +| `zh-HK` | Chinese (Traditional, Hong Kong) | +| `zh-TW` | Chinese (Traditional, Taiwan) | + +If you want to help to translate Web Chat to different language outside of our official translation, please submit a pull request to us. + +To customize the localization strings, please refer to [`LOCALIZATION.md`](LOCALIZATION.md). + +## Community translations + +The following maintainers provide community contributions for languages outside of our official translation. + +| Language code | Translator | +| ------------- | ------------------------------- | +| `ar-eg` | @midineo | +| `ar-jo` | muminasaad, Odai Hatem AbuGaith | +| `zh-yue` | @compulim | + +### Credits to previous community translations + +As we now provide official translation support, the following languages no longer require community contributions. We extend our gratitude to the maintainers for their previous contributions. + +| Language code | Translator | +| ------------- | ---------------------------------------------------------- | +| `bg-bg` | @kalin.krustev | +| `cs-cz` | @msimecek | +| `da-dk` | @Simon_lfr, Thomas Skødt Andersen | +| `de-de` | @matmuenzel | +| `el-gr` | @qdoop | +| `es-es` | @SantiEspada, @ckgrafico, @renrous, @axelsrz, @munozemilio | +| `fi-fi` | @jsur, @sk91swd | +| `fr-fr` | @meulta, @tao1 | +| `he-il` | @geea-develop | +| `hu-hu` | | +| `it-it` | Maurizio Moriconi, @Andrea-Orimoto, @AntoT84 | +| `ja-jp` | @bigplants, @corinagum | +| `ko-kr` | @Eunji | +| `lv-lv` | | +| `nb-no` | @taarskog | +| `nl-nl` | @iMicknl | +| `pl-pl` | @peterblazejewicz | +| `pt-br` | @rcarubbi, @pedropacheco92 | +| `pt-pt` | @bodyzatva, @tiagodenoronha | +| `ru-ru` | @odysseus1973, @seaen | +| `sv-se` | @pekspro | +| `tr-tr` | @vefacaglar | +| `zh-hans` | @Antimoney | +| `zh-hant` | @compulim | ## Adding a new language diff --git a/babel.profile.config.json b/esbuildBabelPluginIstanbul.babel.config.json similarity index 100% rename from babel.profile.config.json rename to esbuildBabelPluginIstanbul.babel.config.json diff --git a/esbuildBabelPluginIstanbul.ts b/esbuildBabelPluginIstanbul.ts index 594ce91037..cc01d0db16 100644 --- a/esbuildBabelPluginIstanbul.ts +++ b/esbuildBabelPluginIstanbul.ts @@ -23,7 +23,7 @@ export const babelPlugin = ({ filter, loader, name, predicate = defaultPredicate } const result = await transformFileAsync(args.path, { - configFile: join(fileURLToPath(import.meta.url), '../babel.profile.config.json'), + configFile: join(fileURLToPath(import.meta.url), '../esbuildBabelPluginIstanbul.babel.config.json'), rootMode: 'root', sourceFileName: args.path }); diff --git a/eslint-local-rules.js b/eslint-local-rules.js index b5d4071ebe..9b152d59c9 100644 --- a/eslint-local-rules.js +++ b/eslint-local-rules.js @@ -1,3 +1,5 @@ +/* eslint-env commonjs */ + module.exports = { 'forbid-use-hook-producer': require('./eslint-rules/forbid-use-hook-producer') }; diff --git a/jest.config.js b/jest.config.js index 04c9bcc9da..c40774f522 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,62 +1,8 @@ -const { join, relative } = require('path'); - module.exports = { collectCoverageFrom: ['/packages/*/src/**/*.{js,jsx,ts,tsx}'], coverageReporters: ['json', 'lcov', 'text-summary', 'clover', 'cobertura'], // We only have 4 instances of Chromium running simultaneously. maxWorkers: 4, - projects: ['/jest.html2.config.js', '/jest.legacy.config.js'], - reporters: [ - 'default', - [ - 'jest-junit', - { - ancestorSeparator: ' › ', - classNameTemplate: '{filepath}', - includeConsoleOutput: true, - outputDirectory: 'coverage', - suiteName: 'BotFramework-WebChat', - titleTemplate: ({ classname, filename, title }) => - [filename, classname, title] - .map(value => (value || '').trim()) - .filter(value => value) - .join(' › ') - } - ], - [ - 'jest-trx-results-processor', - { - outputFile: 'coverage/result.trx', - postProcessTestResult: [ - (testSuiteResult, testResult, testResultNode) => { - // If you want to re-touch the test result, you can refer to source code from these links: - // - https://github.com/facebook/jest/blob/master/packages/jest-types/src/TestResult.ts - // - https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/publish-test-results?view=azure-devops&tabs=yaml#attachments-support - // - https://github.com/Microsoft/vstest/tree/master/src/Microsoft.TestPlatform.Extensions.TrxLogger - // - https://github.com/no23reason/jest-trx-results-processor/tree/master/src - - testResult.failureMessages.forEach(message => { - const pattern = /See (diff|screenshot) for details: (.*)/gmu; - - for (;;) { - const match = pattern.exec(message); - - if (!match) { - break; - } - - testResultNode.ele('ResultFiles').ele('ResultFile').att('path', match[2]); - } - }); - - testResultNode.att( - 'testName', - `${relative(__dirname, testSuiteResult.testFilePath)} › ${testResult.fullName}` - ); - } - ] - } - ], - ['github-actions', { silent: false }] - ] + projects: ['/__tests__/html2.setup/jest.config.js', '/__tests__/unit.setup/jest.config.js'], + reporters: ['default', ['github-actions', { silent: false }]] }; diff --git a/jest.html2.config.js b/jest.html2.config.js deleted file mode 100644 index 5b01c48c79..0000000000 --- a/jest.html2.config.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - displayName: { color: 'cyan', name: 'html2' }, - globals: { npm_package_version: '0.0.0-0.jest' }, - moduleFileExtensions: ['html', 'js'], // Will cause fail validation error if 'js' is not included. - rootDir: './', - setupFilesAfterEnv: [ - '/__tests__/setup/setupImageSnapshot.js', - '/__tests__/setup/setupTestNightly.js', - '/__tests__/setup/setupTimeout.js' - ], - testEnvironment: '/packages/test/harness/src/host/jest/WebDriverEnvironment.js', // Cannot load environment in HTML file due to syntax requirements. Jest also ignores environment comment in transformed file. - testMatch: ['/__tests__/html2/**/*.html'], - testPathIgnorePatterns: ['/node_modules/', '/packages/', '/samples/'], // Jest will warn obsoleted snapshots outside of "testMatch", need "testPathIgnorePatterns" to skip checking obsoleted snapshots. - transform: { - '\\.html$': '/html2-test-transformer.js', - '\\.[jt]sx?$': '/babel-jest-config.js' - } -}; diff --git a/package-lock.json b/package-lock.json index 48c72a66d5..ea858e9a6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,8 +88,6 @@ "husky": "^9.1.7", "jest": "^29.7.0", "jest-image-snapshot": "^6.5.1", - "jest-junit": "^16.0.0", - "jest-trx-results-processor": "^3.0.2", "lint-staged": "^16.1.2", "lolex": "^6.0.0", "minimatch": "^10.0.3", @@ -10958,20 +10956,6 @@ "node": ">=8" } }, - "node_modules/jest-junit": { - "version": "16.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "mkdirp": "^1.0.4", - "strip-ansi": "^6.0.1", - "uuid": "^8.3.2", - "xml": "^1.0.1" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/jest-leak-detector": { "version": "29.7.0", "dev": true, @@ -11596,16 +11580,6 @@ "node": ">=8" } }, - "node_modules/jest-trx-results-processor": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.3", - "uuid": "^8.0.0", - "xmlbuilder": "^15.1.0" - } - }, "node_modules/jest-util": { "version": "29.7.0", "dev": true, @@ -13519,6 +13493,7 @@ "version": "1.0.4", "dev": true, "license": "MIT", + "peer": true, "bin": { "mkdirp": "bin/cmd.js" }, @@ -18533,19 +18508,6 @@ } } }, - "node_modules/xml": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "dev": true, diff --git a/package.json b/package.json index 882970bdc4..0f426db9f9 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "audit-all": "find -name package-lock.json | xargs dirname | xargs -I {} sh -c 'cd {} && npm audit'", "biome": "biome check ./packages", "browser": "node ./packages/test/harness/src/host/dev/index http://localhost:5001/__tests__/html2/", - "browser:watch": "node-dev --no-notify --respawn ./packages/test/harness/src/host/dev/index http://localhost:5001/__tests__/html2/", "build": "npm run build:production && npm run build:html2-samples", "build:html2-samples": "cd __tests__/html2/samples/ && esbuild --bundle --format=esm --outbase=. --outdir=./dist/ --minify **/*.tsx --splitting", "build:production": "npm run build --if-present --workspaces", @@ -168,7 +167,7 @@ "start:test-dev-server": "cd packages && cd test && cd dev-server && npm start", "start:test-harness": "cd packages && cd test && cd harness && npm start", "start:test-page-object": "cd packages && cd test && cd page-object && npm start", - "test": "jest --watch" + "test": "jest" }, "pinDependencies": { "@testing-library/react": [ @@ -257,8 +256,6 @@ "husky": "^9.1.7", "jest": "^29.7.0", "jest-image-snapshot": "^6.5.1", - "jest-junit": "^16.0.0", - "jest-trx-results-processor": "^3.0.2", "lint-staged": "^16.1.2", "lolex": "^6.0.0", "minimatch": "^10.0.3", diff --git a/packages/core/src/utils/dateToLocaleISOString.chatham.spec.js b/packages/core/src/utils/dateToLocaleISOString.chatham.spec.js index d1c8071e25..02fb49d15c 100644 --- a/packages/core/src/utils/dateToLocaleISOString.chatham.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.chatham.spec.js @@ -1,5 +1,5 @@ /** - * @jest-environment ../../../__tests__/setup/jestNodeEnvironmentWithTimezone.js + * @jest-environment ../../../__tests__/unit.setup/JestNodeEnvironmentWithTimeZone.js * @timezone Pacific/Chatham */ diff --git a/packages/core/src/utils/dateToLocaleISOString.japan.spec.js b/packages/core/src/utils/dateToLocaleISOString.japan.spec.js index 6304e26739..c761e290c1 100644 --- a/packages/core/src/utils/dateToLocaleISOString.japan.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.japan.spec.js @@ -1,5 +1,5 @@ /** - * @jest-environment ../../../__tests__/setup/jestNodeEnvironmentWithTimezone.js + * @jest-environment ../../../__tests__/unit.setup/JestNodeEnvironmentWithTimeZone.js * @timezone Asia/Tokyo */ diff --git a/packages/core/src/utils/dateToLocaleISOString.newfoundland.spec.js b/packages/core/src/utils/dateToLocaleISOString.newfoundland.spec.js index 154b100758..875f3aa309 100644 --- a/packages/core/src/utils/dateToLocaleISOString.newfoundland.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.newfoundland.spec.js @@ -1,5 +1,5 @@ /** - * @jest-environment ../../../__tests__/setup/jestNodeEnvironmentWithTimezone.js + * @jest-environment ../../../__tests__/unit.setup/JestNodeEnvironmentWithTimeZone.js * @timezone America/St_Johns */ diff --git a/packages/core/src/utils/dateToLocaleISOString.pacific.spec.js b/packages/core/src/utils/dateToLocaleISOString.pacific.spec.js index 78e7632967..05a583b9a3 100644 --- a/packages/core/src/utils/dateToLocaleISOString.pacific.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.pacific.spec.js @@ -1,5 +1,5 @@ /** - * @jest-environment ../../../__tests__/setup/jestNodeEnvironmentWithTimezone.js + * @jest-environment ../../../__tests__/unit.setup/JestNodeEnvironmentWithTimeZone.js * @timezone America/Los_Angeles */ diff --git a/packages/core/src/utils/dateToLocaleISOString.utc.spec.js b/packages/core/src/utils/dateToLocaleISOString.utc.spec.js index 5e3615826f..fd376b57e0 100644 --- a/packages/core/src/utils/dateToLocaleISOString.utc.spec.js +++ b/packages/core/src/utils/dateToLocaleISOString.utc.spec.js @@ -1,5 +1,5 @@ /** - * @jest-environment ../../../__tests__/setup/jestNodeEnvironmentWithTimezone.js + * @jest-environment ../../../__tests__/unit.setup/JestNodeEnvironmentWithTimeZone.js * @timezone Etc/UTC */ diff --git a/packages/test/harness/src/host/dev/hostOverrides/upload.js b/packages/test/harness/src/host/dev/hostOverrides/upload.js index f68ae14758..9c804e0888 100644 --- a/packages/test/harness/src/host/dev/hostOverrides/upload.js +++ b/packages/test/harness/src/host/dev/hostOverrides/upload.js @@ -1,7 +1,7 @@ const { join, win32 } = require('path'); module.exports = webDriver => async (element, filename) => { - let path = join(process.cwd(), '__tests__/html/assets/uploads/', filename); + let path = join(process.cwd(), '__tests__/downloads/', filename); const { WSL_DISTRO_NAME } = process.env; diff --git a/packages/test/harness/src/host/jest/runHTML.js b/packages/test/harness/src/host/jest/runHTML.js index d7dfb46676..479cfaa521 100644 --- a/packages/test/harness/src/host/jest/runHTML.js +++ b/packages/test/harness/src/host/jest/runHTML.js @@ -48,7 +48,7 @@ global.runHTML = async function runHTML(url, options = DEFAULT_OPTIONS) { const webDriver = (global.webDriver = await allocateWebDriver(options)); try { - const absoluteURL = new URL(url, 'https://webchat2/__tests__/html/'); + const absoluteURL = new URL(url, 'https://webchat2/__tests__/html2/'); global.__operation__ = `loading URL ${absoluteURL.toString()}`; diff --git a/testharness.dockerfile b/testharness.dockerfile deleted file mode 100644 index 20d69017b0..0000000000 --- a/testharness.dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -# Setting to a different base image to secure your container supply chain. -ARG REGISTRY=docker.io -ARG BASE_IMAGE=$REGISTRY/node:18-alpine - -FROM $BASE_IMAGE - -RUN apk update && \ - apk upgrade && \ - apk add --no-cache bash git openssh - -ENV PORT=80 -EXPOSE 80 -RUN npm install serve@10.0.0 -g -ENTRYPOINT ["npx", "--no-install", "serve", "-p", "80", "/var/web"] - -ADD __tests__/setup/web/ /var/web -ADD packages/bundle/dist /var/web -RUN echo {}>/var/web/package.json -WORKDIR /var/web