Знакомство с комплексным тестированием в Angular с помощью Protractor

Знакомство с комплексным тестированием в Angular с помощью Protractor

От автора: Protractor – популярный фреймворк комплексного тестирования. С помощью Protractor Angular приложение можно тестировать в реальном браузере, имитируя взаимодействия, как с реальным пользователем. Комплексное тестирование проверяет, чтобы приложение вело себя ровно так, как ожидает пользователь. Помимо этого, тесты напрямую не относятся к коду.

Знакомство с комплексным тестированием в Angular с помощью Protractor

Protractor запускается поверх популярного Selenium WebDriver (API для автоматизации браузера и тестирования). Помимо функций Selenium WebDriver Protractor предлагает локаторы и методы для захвата UI компонентов приложения Angular. В этом уроке вы узнаете о:

Установке, настройке и запуске Protractor

Написании базовых тестов для Protractor

Практический курс по созданию веб-приложения на Angular4

Станьте профессиональным веб-разработчиком, создавая востребованные веб-приложения на Angular4.

Узнать подробнее

Объектах страниц и о том, как их использовать

Руководствах для написания тестов

Написании E2E тестов для приложения с нуля и до самого конца

Звучит круто? Но сначала о главном.

Нужно ли мне использовать Protractor?

Если вы работали с Angular CLI, то вы должны знать, что по умолчанию с ним идет 2 фреймворка для тестирования. И это:

Юнит тесты на Jasmine и Karma

Комплексные тесты на Protractor

Основное различие – первый используется для тестирования логики компонентов и сервисов, а второй используется для проверки функциональности высокого уровня приложения (в том числе и UI элементы).

Если вы ни разу не тестировали в Angular, рекомендую почитать серию статей «тестирование компонентов в Angular на Jasmine», чтобы лучше понять принцип.

В первом случае можно использовать мощь тестовых утилит Angular и Jasmine для написания не просто юнит тестов для компонентов и сервисов, а также для написания базовых UI тестов. Однако если необходимо протестировать front end функционал приложения полностью, Protractor вам в руки. Protractor API совмещен с шаблонами проектирования, такими как объекты страниц, что упрощает написание тестов и делает их более читаемыми. Пример.

/* 
  1. It should have a create Paste button
  2. Clicking the button should bring up a modal window
*/
 
it('should have a Create Paste button and modal window', () => {
 
 expect(addPastePage.isCreateButtonPresent()).toBeTruthy("The button should exist");
 expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy("The modal window shouldn't exist, not yet!");
 
 addPastePage.clickCreateButton();
 
 expect(addPastePage.isCreatePasteModalPresent()).toBeTruthy("The modal window should appear now"); 
});

Настройка Protractor

Если для генерации проекта вы используете Angular CLI, настройка Protractor не вызовет затруднений. Команду ng new создает следующую структуру папок.

Знакомство с комплексным тестированием в Angular с помощью Protractor

Стандартный шаблон проекта, созданный Protractor, зависит от двух файлов для запуска тестов: специальные файлы из папки e2e и файл конфигураций (protractor.conf.js). Давайте посмотрим, как настраивается файл protractor.conf.js:

/* Path: protractor.conf.ts*/
 
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
 
const { SpecReporter } = require('jasmine-spec-reporter');
 
exports.config = {
  allScriptsTimeout: 11000,
  specs: [
 './e2e/**/*.e2e-spec.ts'
  ],
  capabilities: {
 'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
 showColors: true,
 defaultTimeoutInterval: 30000,
 print: function() {}
  },
  onPrepare() {
 require('ts-node').register({
 project: 'e2e/tsconfig.e2e.json'
 });
 jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};

Если вы умеете запускать тесты в Chrome, то можете оставить все, как есть и пропустить оставшуюся часть раздела.

Настройка Protractor с Selenium Standalone Server

Запись directConnect: true подключает Protractor напрямую к драйверам браузера. Однако на момент написания этого урока Chrome – единственный поддерживаемый браузер. Если нужна поддержка нескольких браузеров или другой браузер, вам понадобится установить Selenium standalone server. Что для этого нужно сделать.

Установите Protractor глобально с помощью npm:

npm install -g protractor

Это установит инструмент командной строки для вебдрайвер менеджера вместе с Protractor. Теперь обновите вебдрайвер менеджер на использование последних дистрибутивов и запустите Selenium standalone server.

webdriver-manager update
webdriver-manager start

Установите directConnect: false и добавьте свойство seleniumAddress:

capabilities: {
 'browserName': 'firefox'
  },
  directConnect: false,
  baseUrl: 'http://localhost:4200/',
  seleniumAddress: 'http://localhost:4444/wd/hub',
  framework: 'jasmine',
  jasmineNodeOpts: {
 showColors: true,
 defaultTimeoutInterval: 30000,
 print: function() {}
  },

Конфиг файл на GitHub дает больше информации о вариантах настройки в Protractor. Я в уроке буду использовать стандартные опции.

Запуск тестов

Если вы работаете в Angular CLI, то для запуска тестов вам нужна команда ng e2e. Если кажется, что тесты медленные, это потому что Angular должен компилировать код при каждом запуске e2e. Если хотите немного ускорить их, вам нужно сделать следующее. Запустите ng serve. Откройте новую вкладку в консоли и запустите:

ng e2e -s false

Теперь тесты должны грузиться быстрее.

Наша цель

Мы будем писать E2E тесты для стандартного приложения Pastebin. Клонируйте объект из репозитория GitHub. Обе версии (стартовая без тестов и финальная с тестами) доступны в отдельных ветках. Клонируйте ветку starter. Можете пробежаться по коду, чтобы вручную ознакомиться с приложением.

Давайте кратко опишем наше приложение Pastebin. Приложение сначала будет загружать список вставок (полученных с ложного сервера) в таблицу. У каждой строки в таблице будет кнопка View Paste, по клику на которую будет открываться модальное окно первичной загрузки. Модальное окно отображает данные вставки с опциями редактирования и удаления. В конце таблицы есть кнопка Create Paste, с помощью которой можно добавить новые вставки.

Знакомство с комплексным тестированием в Angular с помощью Protractor

Оставшаяся часть урока посвящена написанию Protractor тестов в Angular.

Основы Protractor

В файле спецификации, который оканчивается на .e2e-spec.ts, будут храниться реальные тесты приложения. Все тестовые спецификации будут помещаться в папку e2e, так как мы настроили, чтобы Protractor искал спецификации именно в этом месте. При написании тестов на Protractor необходимо учесть 2 вещи:

Синтаксис Jasmine

Protractor API

Синтаксис Jasmine

Создайте новый файл test.e2e-spec.ts с кодом.

/* Path: e2e/test.e2e-spec.ts */
 
import { browser, by, element } from 'protractor';
 
describe('Protractor Demo', () => {
 
  beforeEach(() => {
 //The code here will get executed before each it block is called 
 //browser.get('/');
  });
 
  it('should display the name of the application',() => {
 /*Expectations accept parameters that will be matched with the real value
 using Jasmine's matcher functions. eg. toEqual(),toContain(), toBe(), toBeTruthy() etc. */
 expect("Pastebin Application").toEqual("Pastebin Application");
 
  });
 
  it('should click the create Paste button',() => {
 //spec goes here
 
  });
});

Здесь описывается, как наши тесты будут организованы в файле спецификации с помощью синтаксиса Jasmine. describe(), beforeEach() и it() – глобальные функции Jasmine.

У Jasmine хороший синтаксис для написания тестов, и он отлично работает с Protractor. Если вы не знакомы с Jasmine, рекомендую сначала зайти на страницу Jasmine на GitHub.

Блок describe разбивает тесты на логические сьюты. В каждом блоке describe (или тест сьюте) может быть несколько блоков it (тестовых спецификаций). Сами тесты пишутся в тестовых спецификациях.

Вы спросите: «зачем мне делать такую структуру в тестах?». Тест сьют логически описывает отдельную функцию приложения. Например, все тестовые спецификации по компоненту Pastebin должны, в идеале, быть покрыты в блоке describe с заголовком Pastebin Page. Однако это может привести к избыточности тестов, но сделает их более читаемыми и обслуживаемыми.

В блоке describe может быть метод beforeEach(), который запускается один раз перед каждой спецификацией в этом блоке. То есть если вам перед каждым тестом необходимо редиректить браузер на заданный URL, разместите редирект внутри beforeEach().

Выражения expect, принимающие значение, сцеплены с функциями поиска. Реальное и ожидаемое значение сравниваются, и возвращается булево значение, показывающее, провалился тест или нет.

Protractor API

Давайте «обтянем наш скелет плотью».

/* Path: e2e/test.e2e-spec.ts */
 
import { browser, by, element } from 'protractor';
 
describe('Protractor Demo', () => {
 
  beforeEach(() => {
 browser.get('/');
  });
 
  it('should display the name of the application',() => {
 
 expect(element(by.css('.pastebin')).getText()).toContain('Pastebin Application');
 
  });
 
  it('create Paste button should work',() => {
 
 expect(element(by.id('source-modal')).isPresent()).toBeFalsy("The modal window shouldn't appear right now ");
 element(by.buttonText('create Paste')).click();
 expect(element(by.id('source-modal')).isPresent()).toBeTruthy('The modal window should appear now');
 
  });
});

browser.get(‘/’) и element(by.css(‘.pastebin’)).getText() – часть Protractor API. Давайте испачкаем руки и запрыгнем в Protractor. Популярные компоненты из Protractor API перечислены ниже.

Browser(): вызывается для всех операций на уровне браузера, например, навигация, дебагинг и т.д.

Element(): используется для поиска элемента в DOM по условию поиска или связки условий. Возвращается объект ElementFinder, с ним можно выполнять операции типа getText() или click().

element.all(): ищет массив элементов по связке условий. Возвращает объект ElementArrayFinder. К ElementArrayFinder применимы все операции от ElementFinder.

Локаторы: с помощью локаторов в приложении Angular можно искать элементы.

Практический курс по созданию веб-приложения на Angular4

Станьте профессиональным веб-разработчиком, создавая востребованные веб-приложения на Angular4.

Узнать подробнее

Мы очень часто будем использовать локаторы, поэтому вот самые исползуемые:

By.css(‘selector-name’): самый часто используемый локатор для поиска элемента по названию CSS селектора

by.name(‘name-value’): обнаруживает элемент по значению атрибута name

by.buttonText(‘button-value’): находит элемент button или массив button по внутреннему тексту

Обратите внимание: локаторы by.model, by.binding и by.repeater не работают в приложениях Angular 2+ на момент написания урока. Используйте CSS локаторы. Напишем тесты для нашего приложения Pastebin.

it('should accept and save input values', () => {
 element(by.buttonText('create Paste')).click();
 
 //send input values to the form using sendKeys
 
 element(by.name('title')).sendKeys('Hello world in Ruby');
 element(by.name('language')).element(by.cssContainingText('option', 'Ruby')).click();
 element(by.name('paste')).sendKeys("puts 'Hello world';");
 
 element(by.buttonText('Save')).click();
 
 //expect the table to contain the new paste
 const lastRow = element.all(by.tagName('tr')).last();
 expect(lastRow.getText()).toContain("Hello world in Ruby");
});

Код выше работает, можете сами проверить. Однако не будет ли удобнее писать тесты без словаря Protractor в файле спецификации? О чем я говорю:

it('should have an Create Paste button and modal window', () => {
 
 expect(addPastePage.isCreateButtonPresent()).toBeTruthy("The button should exist");
 expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy("The modal window shouldn't appear, not yet!");
 
 addPastePage.clickCreateButton();
 
 expect(addPastePage.isCreatePasteModalPresent()).toBeTruthy("The modal window should appear now");
 
 
 });
 
it('should accept and save input values', () => {
 
 addPastePage.clickCreateButton();
 
 //Input field should be empty initially
 const emptyInputValues = ["","",""];
 expect(addPastePage.getInputPasteValues()).toEqual(emptyInputValues);
 
 //Now update the input fields
 addPastePage.addNewPaste();
 
 addPastePage.clickSaveButton();
 
 expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy("The modal window should be gone");
 expect(mainPage.getLastRowData()).toContain("Hello World in Ruby");
 
 });

Спецификации кажутся проще без дополнительного мусор от Protractor. Как я это сделал? Позвольте познакомить вас с Page Objects.

Page Objects

Page Objects – шаблон проектирования, популярный среди автоматизаторов кейсов. Объект страницы моделирует страницу или часть приложения с помощью объектно-ориентированного класса. Все объекты (нашего теста), такие как текст, заголовки, таблицы, кнопки и ссылки захватываются в объект страницы. Эти объекты страниц можно импортировать в файл спецификации и выполнять их методы. Это снижает повторения в коде и упрощает его обслуживание.

Создайте папку page-objects и добавьте новый файл pastebin.po.ts. Все объекты компонента Pastebin будут здесь. Как я раньше уже сказал, мы разбили все приложения на 3 компонента, и у каждого компонента будет объект страницы. Схема именования .po.ts, имена можете придумать, какие угодно.

Макет страницы для тестирования.

Знакомство с комплексным тестированием в Angular с помощью Protractor

Код pastebin.po.ts

/* Path e2e/page-objects/pastebin.po.ts*/
 
import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from 'protractor';
 
 
export class Pastebin extends Base {
 
 navigateToHome():promise.Promise<any> {
 return browser.get('/');
 }
 
 getPastebin():ElementFinder {
 return element(by.css('.pastebin'));
 }
 
 /* Pastebin Heading */
 getPastebinHeading(): promise.Promise<string> {
 return this.getPastebin().element(by.css("h2")).getText();
 }
 
 /*Table Data */
 
 getTable():ElementFinder {
 return this.getTable().element(by.css('table'));
 
 }
 
 getTableHeader(): promise.Promise<string> {
 return this.getPastebin().all(by.tagName('tr')).get(0).getText();
 }
 
 getTableRow(): ElementArrayFinder {
 return this.getPastebin().all(by.tagName('tr'));
 }
 
 
 getFirstRowData(): promise.Promise<string> {
 return this.getTableRow().get(1).getText();
 }
 
 getLastRowData(): promise.Promise<string> {
 return this.getTableRow().last().getText();
 }
 
 /*app-add-paste tag*/
 
 getAddPasteTag(): ElementFinder {
 return this.getPastebin().element(by.tagName('app-add-paste'));
 }
 
 isAddPasteTagPresent(): promise.Promise<boolean> {
 return this.getAddPasteTag().isPresent();
 }
 
}

Что мы уже знаем. Protractor API возвращает объекты. Пока что мы работали с объектами трех типов. Это:

promise.Promise

ElementFinder

ElementArrayFinder

Если коротко, element() возвращает ElementFinder, element().all возвращает ElementArrayFinder. С помощью локаторов (by.css, by.tagName и т.д.) можно искать положение элемента в DOM и передавать его в element() или element.all().

ElementFinder и ElementArrayFinder можно сцепить действиями, например, isPresent(), getText(), click() и т.д. Эти метод возвращают промис, который разрешается, когда завершается определенное действие.

Почему у нас в тесте нет цепи then() – потому что Protractor делает это внутри. Тесты кажутся синхронными, хотя это не так. Поэтому наш код будет линейным. Но я рекомендую использовать синтаксис async/await , чтобы защитить код.

Ниже показано, как можно сцепить объекты ElementFinder. Это очень удобно, если в DOM есть несколько селекторов на одно имя, а нам нужно выбрать правильное.

getTable():ElementFinder {
 return this.getPastebin().element(by.css('table'));
 
}

Наш код для объекта страницы готов. Давайте импортируем его в спецификацию. Код для первичных тестов.

/* Path: e2e/mainPage.e2e-spec.ts */
 
import { Pastebin } from './page-objects/pastebin.po';
import { browser, protractor } from 'protractor';
 
 
/* Scenarios to be Tested 
  1. Pastebin Page should display a heading with text Pastebin Application 
  2. It should have a table header
  3. The table should have rows
  4. app-add-paste tag should exist
*/
 
describe('Pastebin Page', () => {
 
  const mainPage: Pastebin = new Pastebin();
 
  beforeEach(() => {
 mainPage.navigateToHome();
  });
 
  it('should display the heading Pastebin Application', () => {
 
 expect(mainPage.getPastebinHeading()).toEqual("Pastebin Application");
 
 
  });
 
 it('should have a table header', () => {
 
 expect(mainPage.getTableHeader()).toContain("id Title Language Code");
 
  })
  it('table should have at least one row', () => {
 
 expect(mainPage.getFirstRowData()).toContain("Hello world");
  })
 
  it('should have the app-add-paste tag', () => {
 expect(mainPage.isAddPasteTagPresent()).toBeTruthy();
  })
});

Организация тестов и рефакторинг

Тесты должны быть организованы так, чтобы общая структура выглядела просто и понятно. Вот пара советов, которых следует придерживаться при организации E2E тестов.

Отделите E2E тесты от юнит тестов

Сгруппируйте E2E тесты разумно. Организуйте тесты по структуре проекта

Если у вас много страниц, у объектов страниц должен быть отдельный путь

Если объекты страниц делят методы (например navigateToHome()), создайте объект базовой страницы. Модели других страниц могут наследоваться от модели базовой страницы

Сделайте тесты независимыми друг от друга. Вам не нужно, чтобы все тесты провалились из-за небольшого изменения в UI, так ведь?

Не используйте проверки в определениях объектов страниц. Проверки должны быть в файле спецификации

Следуя советам выше, мы получаем следующую иерархию страниц и организацию файлов.

Знакомство с комплексным тестированием в Angular с помощью Protractor

О файлах pastebin.po.ts и mainPage.e2e-spec.ts мы уже говорили. Ниже представлены оставшиеся файлы. Base Page Object

/* path: e2e/page-objects/base.po.ts */
 
import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from 'protractor';
 
export class Base {
 
 /* Navigational methods */
 navigateToHome():promise.Promise<any> {
 return browser.get('/');
 }
 
 navigateToAbout():promise.Promise<any>  {
 return browser.get('/about');
 }
 
 navigateToContact():promise.Promise<any>  {
 return browser.get('/contact');
 }
 
 /* Mock data for creating a new Paste and editing existing paste */
 
 getMockPaste(): any {
 let paste: any = { title: "Something  here",language: "Ruby",paste: "Test"}
 return paste;
 }
 
 getEditedMockPaste(): any {
 let paste: any = { title: "Paste 2", language: "JavaScript", paste: "Test2" }
 return paste;
 }
 
 /* Methods shared by addPaste and viewPaste */
 
 getInputTitle():ElementFinder {
 return element(by.name("title"));
 }
 
 getInputLanguage(): ElementFinder {
 return element(by.name("language"));
 }
 
 getInputPaste(): ElementFinder {
 return element(by.name("paste"));
 
 }
}

Объект Add Paste Page

Знакомство с комплексным тестированием в Angular с помощью Protractor

/* Path: e2e/page-objects/add-paste.po.ts */
 
import { browser, by, element, promise, ElementFinder, ElementArrayFinder } from 'protractor';
import { Base } from './base.po';
export class AddPaste extends Base  {
 
 getAddPaste():ElementFinder {
 return element(by.tagName('app-add-paste'));
 }
 
 /* Create Paste button */
 getCreateButton(): ElementFinder {
 return this.getAddPaste().element(by.buttonText("create Paste"));
 }
 
 isCreateButtonPresent() : promise.Promise<boolean> {
 return this.getCreateButton().isPresent();
 }
 
 clickCreateButton(): promise.Promise<void> {
 return this.getCreateButton().click();
 }
 
 /*Create Paste Modal */
 
 getCreatePasteModal(): ElementFinder {
 return this.getAddPaste().element(by.id("source-modal"));
 }
 
 isCreatePasteModalPresent() : promise.Promise<boolean> {
 return this.getCreatePasteModal().isPresent();
 }
 
 /*Save button */
 getSaveButton(): ElementFinder {
 return this.getAddPaste().element(by.buttonText("Save"));
 }
 
 clickSaveButton():promise.Promise<void> {
 return this.getSaveButton().click();
 }
 
 /*Close button */
 
 getCloseButton(): ElementFinder {
 return this.getAddPaste().element(by.buttonText("Close"));
 }
 
 clickCloseButton():promise.Promise<void> {
 return this.getCloseButton().click();
 }
 
 
 /* Get Input Paste values from the Modal window */
 getInputPasteValues(): Promise<string[]> {
 let inputTitle, inputLanguage, inputPaste;
 
 // Return the input values after the promise is resolved
 // Note that this.getInputTitle().getText doesn't work
 // Use getAttribute('value') instead
 return Promise.all([this.getInputTitle().getAttribute("value"), this.getInputLanguage().getAttribute("value"), this.getInputPaste().getAttribute("value")])
 .then( (values) => {
 return values;
 });
 
 }
 
 /* Add a new Paste */
 
 addNewPaste():any {
 let newPaste: any = this.getMockPaste();
 
 //Send input values
 this.getInputTitle().sendKeys(newPaste.title);
 this.getInputLanguage()
 .element(by.cssContainingText('option', newPaste.language)).click();
 this.getInputPaste().sendKeys(newPaste.paste);
 
 //Convert the paste object into an array
 return Object.keys(newPaste).map(key => newPaste[key]);
 
 }
 
}

Файл Add Paste Spec

/* Path: e2e/addNewPaste.e2e-spec.ts */
 
import { Pastebin } from './page-objects/pastebin.po';
import { AddPaste } from './page-objects/add-paste.po';
import { browser, protractor } from 'protractor';
 
/* Scenarios to be Tested 
  1. AddPaste Page should have a button when clicked on should present a modal window 
  2. The modal window should accept the new values and save them
  4. The saved data should appear in the MainPage
  3. Close button should work
*/
 
describe('Add-New-Paste page', () => {
 
  const addPastePage: AddPaste = new AddPaste();
  const mainPage: Pastebin = new Pastebin();
 
  beforeEach(() => {
 
 addPastePage.navigateToHome();
  });
 
  it('should have an Create Paste button and modal window', () => {
 
 expect(addPastePage.isCreateButtonPresent()).toBeTruthy("The button should exist");
 expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy("The modal window shouldn't appear, not yet!");
 
 addPastePage.clickCreateButton();
 
 expect(addPastePage.isCreatePasteModalPresent()).toBeTruthy("The modal window should appear now");
 
 
  });
 
  it("should accept and save input values", () => {
 
 addPastePage.clickCreateButton();
 
 const emptyInputValues = ["","",""];
 expect(addPastePage.getInputPasteValues()).toEqual(emptyInputValues);
 
 const newInputValues = addPastePage.addNewPaste();
 expect(addPastePage.getInputPasteValues()).toEqual(newInputValues);
 
 addPastePage.clickSaveButton();
 
 expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy("The modal window should be gone");
 expect(mainPage.getLastRowData()).toContain("Something here");
 
  });
 
  it("close button should work", () => {
 
 addPastePage.clickCreateButton();
 addPastePage.clickCloseButton();
 
 expect(addPastePage.isCreatePasteModalPresent()).toBeFalsy("The modal window should be gone");
 
  });
 
});

Упражнения

Мы кое-что не учли: тесты для кнопки View Paste и модальное окно, которое появляется после клика на нее. Это будет ваше упражнение. Но я дам подсказку. Структура объектов страницы и спецификаций для ViewPastePage такая же, как для AddPastePage.

Знакомство с комплексным тестированием в Angular с помощью Protractor

Вам необходимо протестировать следующие сценарии:

Страница ViewPaste должна иметь кнопку, по клику должна появляться модалка

Модальное окно должно отображать данные последней вставки

Модальное окно должно позволять обновлять значения

Должна работать кнопка удаления

Попробуйте максимально придерживаться советов. Если сомневаетесь, переключитесь на финальную ветку, чтобы увидеть конечный черновик кода.

Заключение

Вот и все. В этой статье мы научились писать комплексные тесты для Angular приложения с помощью Protractor. Сначала мы поговорили о юнит тестах и e2e тестах, после чего изучили установку, настройку и запуск Protractor. Оставшаяся часть урока была отдана на написание тестов под демо приложение Pastebin.

Автор: Manjunath M

Источник: https://code.tutsplus.com/

Редакция: Команда webformyself.

Практический курс по созданию веб-приложения на Angular4

Станьте профессиональным веб-разработчиком, создавая востребованные веб-приложения на Angular4.

Узнать подробнее
Самые свежие новости IT и веб-разработки на нашем Telegram-канале

Angular 4 с Нуля до Профи

Angular 4 - полное руководство для современной веб-разработки

Научиться

Метки:

Похожие статьи:

Комментарии Вконтакте:

Комментарии Facebook:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Я не робот.

Spam Protection by WP-SpamFree