Usługa Roku
wdio-roku-service to pakiet zewnętrzny, więcej informacji znajdziesz na GitHub | npm Ta usługa nadpisuje wiele części WebdriverIO, aby umożliwić ich użycie z aplikacjami Roku, oraz zapewnia dostęp do Roku ECP do sterowania urządzeniem Roku podczas testowania.
Wymagania
Roku
Kanał testowy/channel.zip i urz ądzenie Roku (z włączonym trybem deweloperskim) w tej samej sieci co twój komputer Mac.
WebdriverIO
To nie jest samodzielny produkt -- jest używany jako wtyczka do frameworka testowego WebdriverIO (lub Usługa, w ich nomenklaturze). Przed użyciem należy przejść przez konfigurację WDIO, uruchamiając npm init wdio@latest
.
Podczas konfiguracji, aby nie musieć przechodzić przez wszystkie pytania/opcje, możesz wybrać następujące opcje podczas fazy inicjalizacji:
- Roku Testing (UWAGA: Użyj tego, jeśli Twoje repozytorium będzie używane tylko do testowania Roku, ponieważ stanie się domyślną i jedyną zainstalowaną usługą. W przeciwnym razie użyj E2E Testing, aby móc zainstalować wiele usług.)
- On my local machine (tylko E2E)
- Web (tylko E2E)
- Chrome (tylko E2E)
- Mocha
- Typescript [moduły działają zarówno dla TS i JS, więc wybierz dowolny]
- autogenerate some test files (Y) -- domyślna lokalizacja
- page objects (Y) -- domyślna lokalizacja
- spec reporter
- additional plugins (N)
- Visual Testing (N)
- services (roku)
- npm install (Y)
Konfiguracja Typescript
Jeśli chcesz używać Typescript do pisania testów, musisz upewnić się, że następujące opcje są ustawione w pliku tsconfig.json wygenerowanym przez Webdriverio.
"moduleResolution": "nodenext",
"module": "NodeNext",
Następnie możesz używać usługi importując ją do swoich testów, jak opisano poniżej.
Konfiguracja WDIO
Obecnie testowanie jest obsługiwane tylko dla jednego urządzenia Roku. Wymagane są następujące aktualizacje konfiguracji:
maxInstances
imaxInstancesPerCapability
powinny wynosić 1. Automatyczne testowanie na wielu urządzeniach nie jest obsługiwane i spowoduje powielanie poleceń wysyłanych do Roku. Powinna być tylko jedna możliwość.
//wdio.conf.js
export const config: WebdriverIO.Config = {
maxInstances: 1,
capabilities: [{
browserName: 'chrome'
// lub jeśli chcesz tryb headless:
browserName: 'chrome',
'goog:chromeOptions': {
args: ['--headless', '--disable-gpu']
}
}],
//...
}
- Zaleca się zwiększenie
waitforInterval
iwaitforTimeout
, ponieważ każdy interwał wymaga pobrania xml z Roku. Aby lepiej wykorzystać funkcjębrowser.debug()
, możesz również wydłużyć limit czasu testrunner mocha do 5+ minut na potrzeby rozwoju.
//wdio.conf.js
export const config: WebdriverIO.Config = {
waitforTimeout: 30000,
//opcjonalne:
mochaOpts: {
ui: 'bdd',
timeout: 600000
},
//...
}
Jesteś gotowy do napisania swojego pierwszego testu!
import { installFromZip } from 'wdio-roku-service/install'
import { exitChannel } from 'wdio-roku-service/channel'
import { Buttons, keyPress, keySequence } from 'wdio-roku-service/controller'
describe('first test', () => {
before('On the landing screen of the test channel', async () => {
await installFromZip(process.env.ROKU_APP_PATH)
})
it('should launch to the homescreen without login', async () => {
await $("//LoadingIndicator").waitForDisplayed({ reverse: true })
await expect($("//ContentCarousel")).toBeDisplayed()
})
after('should return to home', async () => {
await exitChannel()
})
})
Zachęca się również do korzystania z funkcji browser.debug()
w wdio, aby zatrzymać test w celu debugowania i tworzenia testów:
// ...
it('should launch to the homescreen without login', async () => {
await $("//LoadingIndicator").waitForDisplayed({ reverse: true })
await expect($("//ContentCarousel")).toBeDisplayed()
await browser.debug()
// test zatrzymuje się, dostępny staje się REPL do poleceń
Jeśli chrome nie jest w trybie headless, możesz zobaczyć ostatni raz, gdy wywołano openRokuXML()
(prawdopodobnie przez waitForX
lub expect
). Używając REPL w terminalu, możesz korzystać z dowolnych prawidłowych poleceń $
i kilku niestandardowych (browser.openRokuXML()
i browser.saveScreenshot('path/to/ss.jpg')
) -- klasa controller
nie jest dołączona do obiektu browser
, więc obecnie nie możesz ich używać. Na szczęście prawdopodobnie siedzisz obok Roku i masz pilota, którego możesz użyć do nawigacji i od czasu do czasu wywołać browser.openRokuXML()
, aby zobaczyć, co stało się ze stanem strony! Pamiętaj, że XML działa natywnie z xpathing w przeglądarce chrome, więc możesz oceniać/rozwijać swoje selektory bezpośrednio w konsoli chrome podczas debugowania.
.env
Zobacz plik .env.example
. Skopiuj go i zmień nazwę na .env
w swoim projekcie WebdriverIO, który korzysta z tej usługi. Prawdopodobnie będziesz chciał dodać go również do .gitignore.
ROKU_IP
powinien być adresem IP twojego Roku. Polecenia będą używać tego IP do komunikacji z nim. Jest to wymagane.ROKU_USER
iROKU_PW
: Dane logowania są potrzebne do instalacji archiwum, a także do robienia zrzutów ekranu.ROKU_APP_PATH
powinien być bezwzględną ścieżką do pliku zip kanału Roku.ROKU_CHANNEL_ID
powinien być identyfikatorem kanału twojego kanału Roku (zwykle jest to "dev").DEBUG=wdio-roku-service
włączy komunikaty debugowania. Usuń '#' na początku wiersza, jeśli ich potrzebujesz.
Zmienione funkcje
Browser
waitUntil
będzie pobierać xml z Roku przy każdej iteracji, aby sprawdzić zmiany.saveScreenshot
pobierze zrzut ekranu bieżącego ekranu z Roku. Warto zauważyć, że te zrzuty ekranu są w formacie .jpg, a nie .png, którego zwykle używa WebdriverIO.openRokuXML
pobierze xml z Roku, jeśli musisz to zrobić ręcznie, a nie za pomocą waits.
Elements
- Wszystkie oczekiwania są obsługiwane w ten sam sposób jak Browser.
waitForClickable
jest mapowane nawaitForDisplayed
, awaitForStable
jest mapowane nawaitForExist
. click
,doubleClick
imoveTo
nie są obsługiwane. Musisz ręcznie nawigować po aplikacji.isFocused
sprawdzi, czy atrybutfocused
na elemencie ma wartość true.isDisplayed
sprawdzi atrybutbounds
na elemencie i czyvisible
nie jest ustawione na false. JeśliwithinViewport
jest ustawione, granice zostaną porównane z rozmiarem ekranu Roku.getSize
igetLocation
pobierają wartości z atrybutubounds
, zwracając 0 dla rozmiaru i -Infinity dla pozycji, jeśli atrybut nie istnieje.
Inne funkcje nie zostały zmienione, ale wiele z nich nadal działa zgodnie z oczekiwaniami.
Matchers
Większość matcherów została zaktualizowana, aby pobierać xml podczas oczekiwania. Niektóre mają nieco inną funkcjonalność.
toBeDisplayed
,toBeDisplayedInViewport
,toBeFocused
,toBeExisting
,toBePresent
,toExist
,toHaveSize
,toHaveWidth
,toHaveHeight
itoHaveAttribute
działają zgodnie z oczekiwaniami, z uwzględnieniem zmian w Element.toHaveElementProperty
jest mapowane natoHaveAttribute
.toHaveElementClass
sprawdza atrybutname
elementu.toHaveId
jest mapowane natoHaveElementClass
.toHaveText
sprawdza atrybuttext
elementu.toHaveChildren
sprawdza atrybutchildren
elementu.toHaveHTML
będzie traktować xml tak, jakby był HTML, choć prawdopodobnie nie jest zbyt przydatny.
Następujące nie są obecnie obsługiwane:
toBeSelected
- Może być obsługiwane wkrótce po okre śleniu, jak wygląda xml dla wybranych przycisków, jeśli jest różnica.toBeChecked
- Może być obsługiwane wkrótce po określeniu, jak wygląda xml dla zaznaczonych pól wyboru, jeśli jest różnica.toHaveComputedLabel
- Jeśli masz odpowiednik tego na elementach Roku, sprawdź atrybut za pomocątoHaveAttribute
.toHaveComputedRole
- Jeśli masz odpowiednik tego na elementach Roku, sprawdź atrybut za pomocątoHaveAttribute
.toHaveHref
- Jeśli masz URL-e na elementach Roku, sprawdź atrybut za pomocątoHaveAttribute
.toHaveStyle
- Elementy xml nie mają stylów.toHaveClipboardText
- To nie jest znane.toHaveTitle
- Tytuł będzie losowo wygenerowaną tymczasową nazwą pliku xml.toHaveUrl
- URL będzie ścieżką do pliku xml na twoim komputerze.
Użycie
Instalacja kanału
Wymaga to, aby kanał miał przypisany identyfikator.
import { installByID } from 'wdio-roku-service/install';
async before() {
await installByID(process.env.ROKU_CHANNEL_ID);
}
Instalacja z archiwum
Zaleca się przechowywanie ścieżki w pliku .env, szczególnie jeśli masz wielu programistów, którzy mogą mieć różne lokalizacje i/lub nazwy plików.
import { installFromZip } from 'wdio-roku-service/install';
async before() {
await installFromZip(process.env.ROKU_ARCHIVE_PATH);
}
Kanał preinstalowany
Jeśli zainstalowałeś kanał samodzielnie przed testowaniem, możesz po prostu go uruchomić.
import { launchChannel, exitChannel } from 'wdio-roku-service/channel';
async before() {
// Zamknij kanał, jeśli jest już otwarty. Jeśli kanał obsługuje natychmiastowe wznawianie, zostanie tylko przeniesiony w tło
await exitChannel();
// Użycie identyfikatora kanału 'dev' uruchomi sideloadowaną aplikację.
await launchChannel('dev');
}
Testowanie
wdio-roku-service/controller
zapewnia możliwość wysyłania naciśnięć przycisków do Roku. keySequence
jest główną funkcją, wysyłającą kilka naciśnięć przycisków w sekwencji.
import { Buttons, keySequence } from 'wdio-roku-service/controller';
// Nawigacja przez aplikację
await keySequence(Buttons.LEFT, Buttons.LEFT, Buttons.SELECT, Buttons.DOWN, Buttons.SELECT);
// Pobierz aktualny interfejs aplikacji z Roku i załaduj go do przeglądarki
await browser.openRokuXML();
// Lub użyj oczekiwań, które będą wielokrotnie ładować XML, aż upłynie limit czasu lub warunek zostanie spełniony
await browser.waitUntil(condition);
await element.waitForDisplayed();
// używaj matcherów WDIO na XML Roku, jakby to była strona internetowa
await expect(element).toHaveAttr('focused');
wdio-roku-service/controller
ma również funkcje do przytrzymywania lub zwalniania przycisków oraz wprowadzania tekstu za pomocą klawiatury.
import { Buttons, keyboardInput, keyPress, keySequence } from 'wdio-roku-service/controller';
await keySequence(Buttons.DOWN, Buttons.DOWN, Buttons.SELECT);
await keyboardInput('example');
await keyPress(Buttons.ENTER);
await browser.openRokuXML();
Deeplinking
wdio-roku-service/channel
zapewnia funkcjonalność związaną z kanałem. inputChannel
pozwala wysyłać dowolne informacje do aplikacji.
import { exitChannel, launchChannel, MediaType } from 'wdio-roku-service/channel';
await exitChannel();
await launchChannel(process.env.ROKU_CHANNEL_ID, myContent, MediaType.MOVIE, {myExtraParameter:true});
await expect(MyContent.header).toBeDisplayed();
Inne funkcje
wdio-roku-service/info
zapewnia różne funkcje, takie jak pobieranie ikony aplikacji lub osieroconych węzłów.
import { getAppIcon } from 'wdio-roku-service/info';
const response = await getAppIcon(process.env.ROKU_CHANNEL_ID);
expect(response.headers.get('Content-Type')).toBe('image/jpg');
wdio-roku-service/ecp
to bezpośredni interfejs z ECP, jeśli potrzebujesz zrobić coś bardzo specyficznego.
import { ECP } from 'wdio-roku-service/ecp';
await ECP('search/browse?keyword=voyage&type=movie&tmsid=MV000058030000', 'POST');
Typowe pułapki
- Elementy Roku mają swój tekst w atrybucie 'text', a nie między tagami. Podczas korzystania z selektorów,
$('element=Text')
nie zadziała dla prawie każdego elementu. Zamiast tego, musisz użyć$('element[text=Text]')
.
Plan rozwoju funkcji
- Wkrótce zostanie złożony PR, który umożliwi instalację tej usługi podczas ankiety
npm init wdio@latest
. - Obecnie oceniana jest komunikacja Socket z Roku, dzięki czemu można będzie narzędziować więcej funkcji, takich jak środki do wybudzania śpiącego Roku.
- Funkcja(-e) proxy sieciowego, które pozwalają na kluczowanie aktywności sieciowej.
Wykorzystanie raportowania Allure z dołączonymi zrzutami ekranu i plikami XML
Domyślnie, raportowanie Allure nie ma konfiguracji umożliwiającej generowanie zrzutów ekranu aplikacji lub kopii kodu XML reprezentującego aktualny stan aplikacji Roku w dowolnym momencie wykonywania testu. Poniższa dokumentacja wyjaśnia, jak rozwiązać ten problem, aby zrzut ekranu bieżącego stanu aplikacji był generowany i dołączany do raportu Allure za każdym razem, gdy test it
kończy swoje wykonanie. Pozwala również uzyskać migawkę źródłową XML reprezentującego aktualny stan aplikacji Roku za każdym razem, gdy wykonanie testu it
kończy się niepowodzeniem.
Pełna dokumentacja dotycząca Allure Reporter znajduje się na stronie @wdio/allure-reporter https://webdriver.io/docs/allure-reporter/
Zależność Utils.js
Dodaj następujący kod do pliku o nazwie Utils.js
. Ten plik może znajdować się w folderze /helpers
lub podobnym.
/**
* Returns a string representation of the 'now' timestamp in milliseconds for the epoch.
*/
export const getEpochTimestamp = async () => {
return Date.now().toString()
}
/**
* Returns a string representation of the 'now' timestamp following the pattern: {YYYY}-{MM}-{DD}_{hour in 24H}-{Minute}-{Second}-{Milliseconds}
*/
export const getLongFormatTimestamp = async () => {
const now = new Date(Date.now())
const result = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}_${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}-${now.getMilliseconds()}`
return result
}
/**
* An object containing the string representations of possible file extensions used for reporting purposes.
*/
export const FILE_EXTENSIONS = {
JPG: '.jpg',
XML: '.xml'
}
/**
* An object containing the string representations of possible MIME types used for reporting purposes.
*/
export const FILE_MIME_TYPES = {
JPG: 'image/jpeg',
XML: 'application/xml'
}
/**
* A function to generate a filename with a possible prefix, a timestamp and one of the possible extensions provided.
* @param {string} fileExtension Use one of the values from the FILE_EXTENSIONS object defined previously.
* @param {string} [fileNamePrefix] A prefix to be appended at the beginning of the filename if provided. Defaults to an empty string.
*/
export const getFileNameWithTimestamp = async (fileExtension, fileNamePrefix = '') => {
return (fileNamePrefix !== '')
? `${fileNamePrefix}_${await getLongFormatTimestamp()}${fileExtension}`
: `${await getLongFormatTimestamp()}${fileExtension}`
}
Kod wdio.conf.js
Dodaj następujące instrukcje importu do pliku wdio.conf.js
:
import { readFile, rm } from 'node:fs/promises'
import { addAttachment } from '@wdio/allure-reporter'
import { FILE_EXTENSIONS, FILE_MIME_TYPES, getFileNameWithTimestamp } from './<Utils.js file path>/Utils.js' // Zamień <Utils.js file path> na rzeczywistą względną ścieżkę do pliku Utils.js
Zdefiniuj następujący hook afterTest
w pliku wdio.conf.js
. Jeśli masz już działający kod w tym hooku, dołącz poniższy kod.
afterTest: async function (test, context, result) {
// Screenshot saving and attaching logic regardless of test outcome.
const fileName = await getFileNameWithTimestamp(FILE_EXTENSIONS.JPG)
try {
const tempScreenshotPath = `./allure-results/${fileName}`
await browser.saveScreenshot(tempScreenshotPath)
const screenShotData = await readFile(tempScreenshotPath)
addAttachment(`${fileName}`, screenShotData, FILE_MIME_TYPES.JPG)
await rm(tempScreenshotPath).catch((rmError) => {
console.error(`Failed to remove file: ${tempScreenshotPath}`, rmError)
})
} catch (error) {
console.error('Error handling screenshot or attachment: ', error)
}
// XML attaching logic on test failure.
if (result.passed === false) {
const fileName = await getFileNameWithTimestamp(FILE_EXTENSIONS.XML, 'AppStateAfterTestFail')
const rawSourceString = String(await browser.getPageSource())
const extractedXMLSubstring = '<?xml version="1.0" encoding="UTF-8" ?>\n'.concat(rawSourceString.substring(rawSourceString.search('<app-ui xmlns="">'), rawSourceString.search('</app-ui>')).concat('</app-ui>')).replace('<app-ui xmlns="">', '<app-ui>')
try {
addAttachment(`${fileName}`, extractedXMLSubstring, FILE_MIME_TYPES.XML)
} catch (error) {
console.log(error)
}
}
},
Oczekiwane zachowanie
Dzięki temu kodowi w konfiguracji projektu, oczekuje się, że za każdym razem, gdy test it
jest uruchamiany, niezależnie od wyniku testu, zrzut ekranu zostanie wykonany na końcu przebiegu i dołączony do odpowiedniej sekcji w raporcie Allure. W konkretnym przypadku, gdy test nie powiedzie się, migawka źródłowa stanu aplikacji w formacie XML zostanie również dołączona do sekcji testu w raporcie Allure.
Uwagi
- Raporty Allure domyślnie obsługują zrzuty ekranu w formacie
.png
. Nadpisane metody w tej usłudze obsługują obrazy w formacie.jpg
. - Załączniki XML można przeglądać w samym raporcie Allure lub otworzyć w osobnej karcie przeglądarki.