{"id":2173,"date":"2022-09-07T14:51:31","date_gmt":"2022-09-07T12:51:31","guid":{"rendered":"https:\/\/nguenkam.com\/blog\/?p=2173"},"modified":"2022-09-07T14:53:42","modified_gmt":"2022-09-07T12:53:42","slug":"automate-end-to-end-e2e-testing-for-angular-using-protractor-jasmine","status":"publish","type":"post","link":"https:\/\/nguenkam.com\/blog\/index.php\/2022\/09\/07\/automate-end-to-end-e2e-testing-for-angular-using-protractor-jasmine\/","title":{"rendered":"Automate End to end (e2e) testing for Angular using Protractor &#038; Jasmine"},"content":{"rendered":"\n<p><strong>End-to-end testing<\/strong><em>&nbsp;is a methodology used to test whether the flow of an application is performing as designed from start to finish.<\/em><\/p>\n\n\n\n<p>Angular CLI has build in E2E test framework using Protractor and Jasmine.<\/p>\n\n\n\n<p>This example shows Angular end to end testing for  an application nammed \u2018Tour of Heros\u2019.<\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large is-resized\"><img loading=\"lazy\" src=\"https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/e2eTest-1024x547.gif\" alt=\"\" class=\"wp-image-2174\" width=\"682\" height=\"363\" srcset=\"https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/e2eTest-1024x547.gif 1024w, https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/e2eTest-300x160.gif 300w, https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/e2eTest-768x410.gif 768w, https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/e2eTest-1536x820.gif 1536w\" sizes=\"(max-width: 682px) 100vw, 682px\" \/><\/figure><\/div>\n\n\n\n<p>Code base is from<br><a rel=\"noreferrer noopener\" href=\"https:\/\/angular.io\/guide\/testing\" target=\"_blank\">https:\/\/angular.io\/guide\/testing<\/a><br>Git clone:<br><a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/dhormale\/e2e-testing-Angular-Protractor-Jasmine\" target=\"_blank\">https:\/\/github.com\/dhormale\/e2e-testing-Angular-Protractor-Jasmine<\/a><\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>Application generated using&nbsp;<strong><em>\u201cng new\u201d<\/em><\/strong>&nbsp;has below default files for e2e testing:<br><\/p>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large\"><img loading=\"lazy\" width=\"270\" height=\"197\" src=\"https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/image-8.png\" alt=\"\" class=\"wp-image-2175\"\/><\/figure><\/div>\n\n\n\n<p>Default settings from protractor.conf.js and tsconfig.e2e.json should work without any changes.<\/p>\n\n\n\n<p>Apart from above configuration files, we have \/src folder in \/e2e folder which contains\u2026<\/p>\n\n\n\n<ol><li><strong>app.e2e-spec.ts<\/strong> \u2014 Spec file has steps which gets executed. Spec file are written using the jasmine syntax. (Jasmine syntax:&nbsp;<a rel=\"noreferrer noopener\" href=\"https:\/\/jasmine.github.io\/pages\/docs_home.html\" target=\"_blank\">https:\/\/jasmine.github.io\/pages\/docs_home.html<\/a>)<\/li><li><strong>app.po.ts <\/strong>\u2014 Page Object file (methods used to find elements in the page. From spec file we can then call these methods to test the various elements.)<\/li><\/ol>\n\n\n\n<!--more-->\n\n\n\n<p>This example\u2019s app.e2e-spec.ts file has list of specification. <\/p>\n\n\n\n<p>Ref:&nbsp;<a href=\"https:\/\/gist.github.com\/dhormale\/90dd6288ab1780bf0f3039c7cef386c9#file-app-e2e-spec-ts\" rel=\"noreferrer noopener\" target=\"_blank\">https:\/\/gist.github.com\/dhormale\/90dd6288ab1780bf0f3039c7cef386c9#file-app-e2e-spec-ts<\/a><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict'; \/\/ necessary for es6 output in node\n\nimport { browser, element, by, ElementFinder, ElementArrayFinder } from 'protractor';\nimport { promise } from 'selenium-webdriver';\n\nconst expectedH1 = 'Tour of Heroes';\nconst expectedTitle = `${expectedH1}`;\nconst targetHero = { id: 15, name: 'Magneta' };\nconst targetHeroDashboardIndex = 3;\nconst nameSuffix = 'X';\nconst newHeroName = targetHero.name + nameSuffix;\n\nclass Hero {\n  id: number;\n  name: string;\n\n  \/\/ Factory methods\n\n  \/\/ Hero from string formatted as '&lt;id&gt; &lt;name&gt;'.\n  static fromString(s: string): Hero {\n    return {\n      id: +s.substr(0, s.indexOf(' ')),\n      name: s.substr(s.indexOf(' ') + 1),\n    };\n  }\n\n  \/\/ Hero from hero list &lt;li&gt; element.\n  static async fromLi(li: ElementFinder): Promise&lt;Hero&gt; {\n      let stringsFromA = await li.all(by.css('a')).getText();\n      let strings = stringsFromA&#91;0].split(' ');\n      return { id: +strings&#91;0], name: strings&#91;1] };\n  }\n\n  \/\/ Hero id and name from the given detail element.\n  static async fromDetail(detail: ElementFinder): Promise&lt;Hero&gt; {\n    \/\/ Get hero id from the first &lt;div&gt;\n    let _id = await detail.all(by.css('div')).first().getText();\n    \/\/ Get name from the h2\n    let _name = await detail.element(by.css('h2')).getText();\n    return {\n        id: +_id.substr(_id.indexOf(' ') + 1),\n        name: _name.substr(0, _name.lastIndexOf(' '))\n    };\n  }\n}\n\ndescribe('Tutorial part 6', () =&gt; {\n\n  beforeAll(() =&gt; browser.get(''));\n\n  function getPageElts() {\n    let navElts = element.all(by.css('app-root nav a'));\n\n    return {\n      navElts: navElts,\n\n      appDashboardHref: navElts.get(0),\n      appDashboard: element(by.css('app-root app-dashboard')),\n      topHeroes: element.all(by.css('app-root app-dashboard &gt; div h4')),\n\n      appHeroesHref: navElts.get(1),\n      appHeroes: element(by.css('app-root app-heroes')),\n      allHeroes: element.all(by.css('app-root app-heroes li')),\n      selectedHeroSubview: element(by.css('app-root app-heroes &gt; div:last-child')),\n\n      heroDetail: element(by.css('app-root app-hero-detail &gt; div')),\n\n      searchBox: element(by.css('#search-box')),\n      searchResults: element.all(by.css('.search-result li'))\n    };\n  }\n\n  describe('Initial page', () =&gt; {\n\n    it(`has title '${expectedTitle}'`, () =&gt; {\n      expect(browser.getTitle()).toEqual(expectedTitle);\n    });\n\n    it(`has h1 '${expectedH1}'`, () =&gt; {\n        expectHeading(1, expectedH1);\n    });\n\n    const expectedViewNames = &#91;'Dashboard', 'Heroes'];\n    it(`has views ${expectedViewNames}`, () =&gt; {\n      let viewNames = getPageElts().navElts.map((el: ElementFinder) =&gt; el.getText());\n      expect(viewNames).toEqual(expectedViewNames);\n    });\n\n    it('has dashboard as the active view', () =&gt; {\n      let page = getPageElts();\n      expect(page.appDashboard.isPresent()).toBeTruthy();\n    });\n\n  });\n\n  describe('Dashboard tests', () =&gt; {\n\n    beforeAll(() =&gt; browser.get(''));\n\n    it('has top heroes', () =&gt; {\n      let page = getPageElts();\n      expect(page.topHeroes.count()).toEqual(4);\n    });\n\n    it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);\n\n    it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);\n\n    it(`cancels and shows ${targetHero.name} in Dashboard`, () =&gt; {\n      element(by.buttonText('go back')).click();\n      browser.waitForAngular(); \/\/ seems necessary to gets tests to pass for toh-pt6\n\n      let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);\n      expect(targetHeroElt.getText()).toEqual(targetHero.name);\n    });\n\n    it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero);\n\n    it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);\n\n    it(`saves and shows ${newHeroName} in Dashboard`, () =&gt; {\n      element(by.buttonText('save')).click();\n      browser.waitForAngular(); \/\/ seems necessary to gets tests to pass for toh-pt6\n\n      let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);\n      expect(targetHeroElt.getText()).toEqual(newHeroName);\n    });\n\n  });\n\n  describe('Heroes tests', () =&gt; {\n\n    beforeAll(() =&gt; browser.get(''));\n    browser.sleep(1000);\n\n    it('can switch to Heroes view', () =&gt; {\n      getPageElts().appHeroesHref.click();\n      let page = getPageElts();\n      expect(page.appHeroes.isPresent()).toBeTruthy();\n      expect(page.allHeroes.count()).toEqual(10, 'number of heroes');\n    });\n\n    it('can route to hero details', async () =&gt; {\n      getHeroLiEltById(targetHero.id).click();\n\n      let page = getPageElts();\n      expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');\n      let hero = await Hero.fromDetail(page.heroDetail);\n      expect(hero.id).toEqual(targetHero.id);\n      expect(hero.name).toEqual(targetHero.name.toUpperCase());\n    });\n\n    it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView);\n\n    it(`shows ${newHeroName} in Heroes list`, () =&gt; {\n      element(by.buttonText('save')).click();\n      browser.waitForAngular();\n      let expectedText = `${targetHero.id} ${newHeroName}`;\n      expect(getHeroAEltById(targetHero.id).getText()).toEqual(expectedText);\n    });\n\n    it(`deletes ${newHeroName} from Heroes list`, async () =&gt; {\n      const heroesBefore = await toHeroArray(getPageElts().allHeroes);\n      const li = getHeroLiEltById(targetHero.id);\n      li.element(by.buttonText('x')).click();\n\n      const page = getPageElts();\n      expect(page.appHeroes.isPresent()).toBeTruthy();\n      expect(page.allHeroes.count()).toEqual(9, 'number of heroes');\n      const heroesAfter = await toHeroArray(page.allHeroes);\n      \/\/ console.log(await Hero.fromLi(page.allHeroes&#91;0]));\n      const expectedHeroes =  heroesBefore.filter(h =&gt; h.name !== newHeroName);\n      expect(heroesAfter).toEqual(expectedHeroes);\n      \/\/ expect(page.selectedHeroSubview.isPresent()).toBeFalsy();\n    });\n\n    it(`adds back ${targetHero.name}`, async () =&gt; {\n      const newHeroName = 'Alice';\n      const heroesBefore = await toHeroArray(getPageElts().allHeroes);\n      const numHeroes = heroesBefore.length;\n\n      element(by.css('input')).sendKeys(newHeroName);\n      element(by.buttonText('add')).click();\n\n      let page = getPageElts();\n      let heroesAfter = await toHeroArray(page.allHeroes);\n      expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes');\n\n      expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there');\n\n      const maxId = heroesBefore&#91;heroesBefore.length - 1].id;\n      expect(heroesAfter&#91;numHeroes]).toEqual({id: maxId + 1, name: newHeroName});\n    });\n\n    it('displays correctly styled buttons', async () =&gt; {\n      element.all(by.buttonText('x')).then(buttons =&gt; {\n        for (const button of buttons) {\n          \/\/ Inherited styles from styles.css\n          expect(button.getCssValue('font-family')).toBe('Arial');\n          expect(button.getCssValue('border')).toContain('none');\n          expect(button.getCssValue('padding')).toBe('5px 10px');\n          expect(button.getCssValue('border-radius')).toBe('4px');\n          \/\/ Styles defined in heroes.component.css\n          expect(button.getCssValue('left')).toBe('194px');\n          expect(button.getCssValue('top')).toBe('-32px');\n        }\n      });\n\n      const addButton = element(by.buttonText('add'));\n      \/\/ Inherited styles from styles.css\n      expect(addButton.getCssValue('font-family')).toBe('Arial');\n      expect(addButton.getCssValue('border')).toContain('none');\n      expect(addButton.getCssValue('padding')).toBe('5px 10px');\n      expect(addButton.getCssValue('border-radius')).toBe('4px');\n    });\n\n  });\n\n  describe('Progressive hero search', () =&gt; {\n\n    beforeAll(() =&gt; browser.get(''));\n\n    it(`searches for 'Ma'`, async () =&gt; {\n      getPageElts().searchBox.sendKeys('Ma');\n      browser.sleep(1000);\n\n      expect(getPageElts().searchResults.count()).toBe(4);\n    });\n\n    it(`continues search with 'g'`, async () =&gt; {\n      getPageElts().searchBox.sendKeys('g');\n      browser.sleep(1000);\n      expect(getPageElts().searchResults.count()).toBe(2);\n    });\n\n    it(`continues search with 'e' and gets ${targetHero.name}`, async () =&gt; {\n      getPageElts().searchBox.sendKeys('n');\n      browser.sleep(1000);\n      let page = getPageElts();\n      expect(page.searchResults.count()).toBe(1);\n      let hero = page.searchResults.get(0);\n      expect(hero.getText()).toEqual(targetHero.name);\n    });\n\n    it(`navigates to ${targetHero.name} details view`, async () =&gt; {\n      let hero = getPageElts().searchResults.get(0);\n      expect(hero.getText()).toEqual(targetHero.name);\n      hero.click();\n\n      let page = getPageElts();\n      expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');\n      let hero2 = await Hero.fromDetail(page.heroDetail);\n      expect(hero2.id).toEqual(targetHero.id);\n      expect(hero2.name).toEqual(targetHero.name.toUpperCase());\n    });\n  });\n\n  async function dashboardSelectTargetHero() {\n    let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex);\n    expect(targetHeroElt.getText()).toEqual(targetHero.name);\n    targetHeroElt.click();\n    browser.waitForAngular(); \/\/ seems necessary to gets tests to pass for toh-pt6\n\n    let page = getPageElts();\n    expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail');\n    let hero = await Hero.fromDetail(page.heroDetail);\n    expect(hero.id).toEqual(targetHero.id);\n    expect(hero.name).toEqual(targetHero.name.toUpperCase());\n  }\n\n  async function updateHeroNameInDetailView() {\n    \/\/ Assumes that the current view is the hero details view.\n    addToHeroName(nameSuffix);\n\n    let page = getPageElts();\n    let hero = await Hero.fromDetail(page.heroDetail);\n    expect(hero.id).toEqual(targetHero.id);\n    expect(hero.name).toEqual(newHeroName.toUpperCase());\n  }\n\n});\n\nfunction addToHeroName(text: string): promise.Promise&lt;void&gt; {\n  let input = element(by.css('input'));\n  return input.sendKeys(text);\n}\n\nfunction expectHeading(hLevel: number, expectedText: string): void {\n    let hTag = `h${hLevel}`;\n    let hText = element(by.css(hTag)).getText();\n    expect(hText).toEqual(expectedText, hTag);\n};\n\nfunction getHeroAEltById(id: number): ElementFinder {\n  let spanForId = element(by.cssContainingText('li span.badge', id.toString()));\n  return spanForId.element(by.xpath('..'));\n}\n\nfunction getHeroLiEltById(id: number): ElementFinder {\n  let spanForId = element(by.cssContainingText('li span.badge', id.toString()));\n  return spanForId.element(by.xpath('..\/..'));\n}\n\nasync function toHeroArray(allHeroes: ElementArrayFinder): Promise&lt;Hero&#91;]&gt; {\n  let promisedHeroes = await allHeroes.map(Hero.fromLi);\n  \/\/ The cast is necessary to get around issuing with the signature of Promise.all()\n  return &lt;Promise&lt;any&gt;&gt; Promise.all(promisedHeroes);\n}<\/code><\/pre>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h4>Let\u00b4s run the test<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>ng e2e<\/code><\/pre>\n\n\n\n<p id=\"aae6\">This command will do following things<\/p>\n\n\n\n<ol><li>Run spec in browser (default browser configured is chrome which run with message \u2018<em>Chrome is being controlled by automated test software<\/em>\u2019)<\/li><li>Log test execution result on console as below.<\/li><\/ol>\n\n\n\n<div class=\"wp-block-image\"><figure class=\"aligncenter size-large is-resized\"><img loading=\"lazy\" src=\"https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/image-9.png\" alt=\"\" class=\"wp-image-2176\" width=\"558\" height=\"586\" srcset=\"https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/image-9.png 638w, https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/image-9-286x300.png 286w, https:\/\/nguenkam.com\/blog\/wp-content\/uploads\/2022\/09\/image-9-24x24.png 24w\" sizes=\"(max-width: 558px) 100vw, 558px\" \/><\/figure><\/div>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h4>To run e2e testing with&nbsp;<strong>headless&nbsp;<\/strong>mode.<\/h4>\n\n\n\n<p>For continues integration we need to run e2e tests in headless mode. (without opening Chrome browser). To make this work we need to update&nbsp;<em>protractor.conf.js<\/em>&nbsp;files&nbsp;<em>capabilities&nbsp;<\/em>section as below.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>capabilities: {\n   'browserName': 'chrome',\n   chromeOptions: {\n   binary: process.env.CHROME_BIN,\n   args: &#91;'--headless', '--no-sandbox']\n   }\n},<\/code><\/pre>\n\n\n\n<p>From command prompt we can have this dynamic as : <\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ng e2e --capabilities.chromeOptions.args=\"--headless\"<\/code><\/pre>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h4>To run e2e on DEV\/QA servers or seperate environment.<\/h4>\n\n\n\n<p>protractor.conf.js file has baseUrl: \u2018http:\/\/localhost:4200\/&#8217;<br>This can be changed to one we want. To make pass it dynamically from command prompt, we can pass baseUrl as below.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node node_modules\/protractor\/bin\/protractor --baseUrl=\"https:\/\/google.com\/\"<\/code><\/pre>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h4>Cheatsheet for accessing elements:<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>element(by.id('firstName'))\n\nelement(by.css('.signout'))\n\nelement(by.model('address.city')) \n\nelement(by.binding('address.city')); \n\nelement(by.input('firstName'));\n\nelement(by.input('firstName')).clear();\n\nelement(by.buttonText('Close'));\n\nelement(by.partialButtonText('Save'));\n\nelement(by.linkText('Save'));\n\nelement(by.partialLinkText('Save'));\n\nelement(by.css('img&#91;src*='assets\/img\/profile.png']')); \nelement(by.css('.pet .cat'));\nelement(by.cssContainingText('.pet', 'Dog'));\n\nallColors = element.all(by.options('c c in colors'));<\/code><\/pre>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h4>Cheatsheet for typing (sendKeys):<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>element(by.id('firstName').sendKeys(\"John\");\n\nsendKeys(Key.ENTER);\n\nsendKeys(Key.TAB);\nsendKeys(Key.BACK_SPACE)\nelement(by.id('user_name')).clear()<\/code><\/pre>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h4>Cheatsheet for collection:<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>var list = element.all(by.css('.items));\n\nvar list2 = element.all(by.repeater('personhome.results'));\n\nexpect(list.count()).toBe(3);\n\nexpect(list.get(0).getText()).toBe('First\u2019)\n\nexpect(list.get(1).getText()).toBe('Second\u2019)\n\nexpect(list.first().getText()).toBe('First\u2019)\n\nexpect(list.last().getText()).toBe('Last\u2019)<\/code><\/pre>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p><strong><span class=\"has-inline-color has-vivid-red-color\">PS:<\/span><\/strong> <em>For accessing angular material elements, below link has e2e specs from material team<\/em>.&nbsp;<a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/angular\/material2\/tree\/master\/e2e\/components\" target=\"_blank\">https:\/\/github.com\/angular\/material2\/tree\/master\/e2e\/components<\/a><\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h5>Reference:<\/h5>\n\n\n\n<p><a href=\"https:\/\/medium.com\/\">https:\/\/medium.com\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>End-to-end testing&nbsp;is a methodology used to test whether the flow of an application is performing as designed from start to finish. Angular CLI has build in E2E test framework using Protractor and Jasmine. This example shows Angular end to end testing for an application nammed \u2018Tour of Heros\u2019. Code base is fromhttps:\/\/angular.io\/guide\/testingGit clone:https:\/\/github.com\/dhormale\/e2e-testing-Angular-Protractor-Jasmine Application generated [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2177,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[37],"tags":[594,597,596,595,598],"_links":{"self":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/2173"}],"collection":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=2173"}],"version-history":[{"count":2,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/2173\/revisions"}],"predecessor-version":[{"id":2180,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/2173\/revisions\/2180"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/media\/2177"}],"wp:attachment":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=2173"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=2173"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=2173"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}