{"id":699,"date":"2014-09-11T06:59:03","date_gmt":"2014-09-11T10:59:03","guid":{"rendered":"http:\/\/qxf2.com\/blog\/?p=699"},"modified":"2024-10-16T16:12:09","modified_gmt":"2024-10-16T20:12:09","slug":"page-object-model-selenium","status":"publish","type":"post","link":"https:\/\/qxf2.com\/blog\/page-object-model-selenium\/","title":{"rendered":"Implementing the Page Object Model (Selenium + Python)"},"content":{"rendered":"<div style=\"background-color: #ffff99;\">\n<h3>UPDATE:<\/h3>\n<p> We are retiring this post in favor of a newer post: <a href=\"https:\/\/qxf2.com\/blog\/page-object-model-selenium-python\/\">Page Object Model (Selenium, Python)<\/a><br \/>\nPlease refer to the newer post. It has a more detailed architectural breakdown, provides many more code snippets and write an automated test for a very relatable application &#8211; Gmail. You could also visit our open-sourced <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\">Python + Selenium test automation framework<\/a> based on the page object pattern and read through it&#8217;s <a href=\"https:\/\/github.com\/qxf2\/qxf2-page-object-model\/wiki\">wiki<\/a>.\n<\/div>\n<hr>\n<p><strong>Problem:<\/strong> Testers think changing existing test scripts to use the page object model is complicated.<\/p>\n<hr>\n<h3>Why this post?<\/h3>\n<p>Tutorials on the page object model usually show you how to implement the page object model using a cliched login page as an example. Most online tutorials rarely show you how to modify your existing tests to use the page object model. This post is a follow up to our <a href=\"https:\/\/qxf2.com\/blog\/page-object-tutorial\/\">descriptive introduction to the page object model<\/a>. In this tutorial, we provide a hands on example of how to revamp your existing test scripts to use the page object model.<\/p>\n<hr>\n<h3>A refresher<\/h3>\n<p>Within your web app&#8217;s UI there are areas that your tests interact with. A Page Object simply models these as objects within the test code. This reduces the amount of duplicated code and means that if the UI changes, the fix need only be applied in one place.[<a href=\"https:\/\/code.google.com\/p\/selenium\/wiki\/PageObjects\">1<\/a>]<\/p>\n<p>The page object model allows us to do two things mainly:[<a href=\"http:\/\/www.ralphlavelle.net\/2012\/08\/the-page-object-pattern-for-ui-tests.html\">2<\/a>]<br \/>\n1) Encapsulate the low-level &#8220;internals&#8221; of the page &#8211; the ids of the buttons, classes of element, etc.<br \/>\n2) Make the client (of the Page Object) test methods more elegant by taking them to a higher level.<\/p>\n<hr>\n<h3> Test scenario<\/h3>\n<p>I am going to test the chess viewer widget on Chessgames.com. I am choosing to check if Black&#8217;s 21st move in the <a href=\"http:\/\/www.chessgames.com\/perl\/chessgame?gid=1477101\">Casino Royale Chess Game<\/a> is executed correctly. Black&#8217;s queen moves from the b6 square to the f2 square. We will verify that the b6 square is empty and that the f2 square has a black queen on it.<br \/>\nPS: Black&#8217;s 21st move in this game is the reason behind our company name <a href=\"http:\/\/www.qxf2.com\">Qxf2<\/a>.<\/p>\n<p><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/07\/qxf2_move.gif\" data-rel=\"lightbox-image-0\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-medium wp-image-714\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/07\/qxf2_move-300x174.gif\" alt=\"qxf2_move\" width=\"300\" height=\"174\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/07\/qxf2_move-300x174.gif 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/07\/qxf2_move-1024x597.gif 1024w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<hr>\n<h3>Implementing the page object model<\/h3>\n<p>For this tutorial, we will show the following:<br \/>\n1. Write a working test<br \/>\n2. Prepare to move to the page object model<br \/>\n3. Implement the page objects<br \/>\n4. Run the test<br \/>\n5. Compare with the first implementation<\/p>\n<p><strong>1. Write a working test<\/strong><br \/>\nI am going to implement a working test. We will use this as a starting point. Let&#8217;s pretend that this is your existing test. I have written the following qxf2move.py<\/p>\n<p><strong>qxf2move.py<\/strong><\/p>\n<pre lang=\"python\">import unittest\r\nfrom selenium import webdriver\r\nfrom selenium.webdriver.common.keys import Keys\r\n\r\nclass qxf2move(unittest.TestCase):\r\n\r\n    def setUp(self):\r\n        self.driver=webdriver.Firefox()\r\n\r\n    def testUserMove21Black(self):\r\n        driver=self.driver\r\n        driver.get(\"http:\/\/www.chessgames.com\/perl\/chessgame?gid=1477101\")\r\n        self.assertIn(\"Mikhail Krasenkow vs Hikaru Nakamura (2007) \\\"Casino Royale\\\"\",driver.title)\r\n        \r\n        #Click on Black's 21st move\r\n        driver.find_element_by_id(\"Var0Mv42\").click()\r\n\r\n        #Verify the squares b6 and f2 ([2,1], [6,5]) have the right pieces\r\n        chess_square = driver.find_element_by_id(\"img_tcol5trow6\")\r\n        #Verify that the square f2 has a black queen (bq.png)\r\n        self.assertEqual(\"http:\/\/www.chessgames.com\/pgn4web-2.82\/images\/bq.png\",chess_square.get_attribute('src'))\r\n\r\n        chess_square = driver.find_element_by_id(\"img_tcol1trow2\")\r\n        #Verify that the square b6 is empty (clear.png)\r\n        self.assertEqual(\"http:\/\/www.chessgames.com\/pgn4web-2.82\/images\/clear.png\",chess_square.get_attribute('src'))\r\n              \r\n    def tearDown(self):\r\n        self.driver.close()\r\n\r\nif __name__==\"__main__\":\r\n    unittest.main()\r\n\r\n<\/pre>\n<p>Here as you can see the test scripts contains the actual details of the attributes and ids as they appear in the page source. It is not easy to maintain these scripts in case of underlying changes. Hence we try to make the test script more readable and extendable by making a few changes.<\/p>\n<p><strong>2. Prepare to move to the page object model <\/strong><br \/>\n&#8211; Identify the various objects and elements on the web page. Define them as objects and variables in the test script. E.g.: Title, Game_Id, Move_Str, Chess Board, Chess Piece, locators for the various objects<\/p>\n<p><a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/08\/chess-page-11.jpg\" data-rel=\"lightbox-image-1\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/08\/chess-page-11-300x210.jpg\" alt=\"chess_pageobjectmodel\" width=\"300\" height=\"210\" class=\"aligncenter size-medium wp-image-1119\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/08\/chess-page-11-300x210.jpg 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/08\/chess-page-11-1024x718.jpg 1024w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<p>&#8211; Identify the various actions possible on these objects using the available elements. These actions can be defined as the methods in the test script. E.g.: get_Title, getCurrentBoard, create_mov_str, gotoMove, gotoStart, gotoEnd<\/p>\n<p><strong>3. Implement the different pages as page objects<\/strong><br \/>\n-Separate the test case and the page objects, methods in different files. Here my test case is present in TestChessGamePage.py and the page objects, methods are present in ChessGamePage.py<\/p>\n<p><strong>TestChessGamePage.py<\/strong><\/p>\n<pre lang=\"python\">from selenium import webdriver\r\nfrom selenium.webdriver.common.by import By\r\nimport unittest\r\nimport ChessGamePage\r\n\r\nclass TestChessGamePage(unittest.TestCase):\r\n\r\n    def setUp(self):\r\n        self.driver = webdriver.Firefox()\r\n        \r\n    def testUserMove21Black(self):\r\n        \"Test Black's 21st move (qxf2)\"\r\n        gameid = \"1477101\"\r\n        player = \"black\"\r\n        movenum = 21\r\n        \"\"\"List of the board position in the format (Piece,row,col)\r\n        where top left corner of the board is row =0, col= 0,\r\n        white player = 'w',black player = 'b'\r\n        pawn(p), rook(r), knight(n), bishop(b), queen(q), and king(k)\"\"\"\r\n        lst = [('br',0,4), ('bk',0,6),\r\n               ('bn',1,3), ('bb',1,4), ('bp',1,5), ('bp',1,6), ('bp',1,7),\r\n               ('bb',2,0), ('br',2,2), ('wb',2,5),\r\n               ('bp',3,0),\r\n               ('bp',4,2),\r\n               ('wp',5,6),\r\n               ('wp',6,0), ('wn',6,3), ('bq',6,5), ('wb',6,6), ('wp',6,7),\r\n               ('wr',7,1), ('wq',7,3), ('wr',7,4), ('wk',7,6)]\r\n\r\n        chessgame_page = ChessGamePage.ChessGamePage(self.driver,gameid)\r\n        chessgame_page.openpage()\r\n        page_title = chessgame_page.get_Title()\r\n        self.assertIn(\"Mikhail Krasenkow vs Hikaru Nakamura (2007) \\\"Casino Royale\\\"\", page_title)\r\n        #Goto Black's 21st move \r\n        chessgame_page.goto_Move(player,movenum)\r\n        actualBoardb21 = chessgame_page.getCurrentBoard()\r\n        expectedBoardb21 = ChessGamePage.ChessBoard()\r\n        expectedBoardb21.populate(lst)\r\n        #Compare with the expected chess board\r\n        self.assertEqual(actualBoardb21,expectedBoardb21)\r\n\r\n    def tearDown(self):\r\n        self.driver.close()\r\n               \r\n#---START OF SCRIPT\r\nif __name__==\"__main__\":\r\n    suite = unittest.TestLoader().loadTestsFromTestCase (TestChessGamePage)\r\n    unittest.TextTestRunner(verbosity=2).run(suite)\r\n\r\n<\/pre>\n<p>The above file does not contain the driver calls to get the elements in the page. <u>For the sake of simplicity, we have not shown you how to implement a driver factory<\/u>. <\/p>\n<p>All the finer details are added to the files Base_Page_Object.py and ChessGamePage.py as shown below<\/p>\n<p><strong>Base_Page_Object.py<\/strong><\/p>\n<pre lang=\"python\">\r\nfrom selenium import webdriver\r\nfrom selenium.webdriver.common.by import By\r\n \r\nclass Page(object):\r\n    \"\"\"\r\n    Base class that all page models can inherit from\r\n    \"\"\"\r\n    def __init__(self, selenium_driver, base_url='http:\/\/www.chessgames.com\/'):\r\n        self.base_url = base_url\r\n        self.driver = selenium_driver\r\n        self.timeout = 30\r\n            \r\n    def find_element(self, *loc):\r\n        return self.driver.find_element(*loc)\r\n \r\n    def open(self,url):\r\n        url = self.base_url + url\r\n        self.driver.get(url)\r\n  \r\n<\/pre>\n<p><strong>ChessGamePage.py<\/strong><\/p>\n<pre lang=\"python\"> \r\nfrom selenium import webdriver\r\nfrom selenium.webdriver.common.by import By\r\nfrom Base_Page_Object import Page\r\n    \r\nclass ChessGamePage(Page):\r\n    #Elements\r\n    move_str = \"\"\r\n    url = \"\"\r\n    \"\"\" Var0Mv(N*2-1) is the id for Nth white move\r\n        Var0Mv(N*2)   is the id for Nth black move \"\"\"\r\n    id_str = \"Var0Mv\"\r\n    #Locators\r\n    startButton_loc = (By.ID, 'startButton')\r\n    backbButton_loc = (By.ID, 'backButton')\r\n    autoplayButton_loc = (By.ID, 'autoplayButton')\r\n    forwardButton_loc = (By.ID, 'forwardButton')\r\n    endButton_loc = (By.ID, 'endButton')\r\n    \r\n    \r\n    def __init__(self, selenium_driver,gameid,base_url='http:\/\/www.chessgames.com\/'):\r\n        Page.__init__(self, selenium_driver, base_url='http:\/\/www.chessgames.com\/') \r\n        self.url = 'perl\/chessgame?gid=' + gameid\r\n        \r\n    # Actions\r\n    def openpage(self):\r\n        self.open(self.url)\r\n \r\n    def get_Title(self):\r\n        \"\"\"Returns the page title\"\"\"        \r\n        return self.driver.title\r\n\r\n    def getCurrentBoard(self):\r\n        \"\"\"Returns the current chessboard positions\"\"\"\r\n        board = ChessBoard(self.driver)\r\n        board.loadFromPage()\r\n        return board\r\n\r\n    def create_move_str(self,player,move):\r\n        \"\"\"Creates the value string using the player and move information\"\"\"\r\n        if player.lower() == \"white\":\r\n            self.move_str = self.id_str + str((move*2)-1)\r\n        else:\r\n            self.move_str = self.id_str + str(move*2)\r\n        \r\n    def goto_Move(self,player,move):\r\n        \"\"\"Clicks on the particular move\"\"\"\r\n        self.create_move_str(player,move)\r\n        move_loc = (By.ID,self.move_str)\r\n        self.find_element(*move_loc).click()\r\n        \r\n    def goto_Start(self):\r\n        \"\"\"Clicks the start of the game button\"\"\"\r\n        self.find_element(*self.startButton_loc).click()\r\n\r\n    def goto_Back(self):\r\n        \"\"\"Clicks the ack one step bbutton\"\"\"\r\n        self.find_element(*self.backButton_loc).click()\r\n\r\n    def goto_AutoPlay(self):\r\n        \"\"\"Clicks the autoplay button\"\"\"\r\n        self.find_element(*self.autoplayButton_loc).click()\r\n\r\n    def goto_Forward(self):\r\n        \"\"\"Clicks the one step forward button\"\"\"\r\n        self.find_element(*self.forwardButton_loc).click()\r\n\r\n    def goto_End(self):\r\n        \"\"\"Clicks the end of game button\"\"\"\r\n        self.find_element(*self.endButton_loc).click()\r\n        \r\n\"\"\"This class represents the actual chess board present on the page\"\"\"\r\nclass ChessBoard(object):\r\n    \"\"\"This string is the id for the image(piece) present in each square(col,row) of the chess board\"\"\"\r\n    id_str = \"img_tcol%strow%s\"\r\n    \r\n    def __init__(self,driver=None):\r\n        self.driver = driver\r\n        self.grid = []\r\n        \r\n    def loadFromPage(self):\r\n        \"\"\"Creates the chess matrix from the page data\"\"\"\r\n        for row in xrange(8):\r\n            for col in xrange(8):\r\n                name = self.getSquare(row,col)\r\n                if name:\r\n                    piece = ChessPiece(name,row,col)\r\n                    self.grid.append(piece)\r\n                    \r\n    def getSquare(self, row, col):\r\n        \"\"\"Returns the piece present at the particular row,col. None if the square is vacant\"\"\"\r\n        elem = self.driver.find_element_by_id(self.id_str%(str(col),str(row)))\r\n        src = elem.get_attribute('src')\r\n        name = src.split(\"\/\")[-1].split(\".\")[0]\r\n        return name if (name.lower() != 'clear') else None\r\n    \r\n    def populate(self,lst):\r\n        \"\"\"Creates the chess matrix as per the user data\"\"\"\r\n        for entry in lst:\r\n            name,row,col = entry\r\n            piece = ChessPiece(name,row,col)\r\n            self.grid.append(piece)\r\n    def __str__(self):\r\n        return str(self.grid)\r\n    def __eq__(self,obj):\r\n        return self.grid == obj.grid\r\n\r\n\"\"\"This class represents the chessmen deployed on a chessboard for playing the game of chess\"\"\"\r\nclass ChessPiece(object):\r\n    def __init__(self,name,row,col):\r\n        self.name = name\r\n        self.row = row\r\n        self.col = col\r\n    def __eq__(self,obj):\r\n        return (self.name == obj.name and self.row == obj.row and self.col == obj.col)\r\n    def __hash__(self):\r\n        tmp = 'row' + `self.row` + 'col' + `self.col` + self.name\r\n        return hash(tmp)\r\n    def __str__(self):\r\n        return \"(%s,%d,%d)\" % (self.name, self.row, self.col)\r\n    def __repr__(self):\r\n        return \"(%s,%d,%d)\" % (self.name, self.row, self.col)\r\n<\/pre>\n<p><strong>4. Run the test<\/strong><br \/>\nHere is the snapshot of the test run<br \/>\n<a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/08\/test_run_pom.gif\" data-rel=\"lightbox-image-2\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-medium wp-image-985\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/08\/test_run_pom-300x114.gif\" alt=\"run the page object model test\" width=\"300\" height=\"114\" \/><\/a><\/p>\n<p><strong>5. Compare with the first implementation<\/strong><br \/>\nLet us compare the files qxf2move.py and TestChessGamePage.py<br \/>\n<a href=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/09\/file_compare.png\" data-rel=\"lightbox-image-3\" data-rl_title=\"\" data-rl_caption=\"\" title=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/09\/file_compare-300x155.png\" alt=\"file_compare\" width=\"300\" height=\"155\" class=\"aligncenter size-medium wp-image-1497\" srcset=\"https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/09\/file_compare-300x155.png 300w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/09\/file_compare-1024x531.png 1024w, https:\/\/qxf2.com\/blog\/wp-content\/uploads\/2014\/09\/file_compare.png 1346w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<p>You&#8217;ll notice that using the page object model, it becomes easy to add new testcases without bothering about the details of the page elements. Also any changes to the page elements do not change the testcases &#8211; only the logic in ChessGamePage.py needs to be modified.<\/p>\n<hr>\n<h3>References:<\/h3>\n<p>1. <a href=\"https:\/\/code.google.com\/p\/selenium\/wiki\/PageObjects\">https:\/\/code.google.com\/p\/selenium\/wiki\/PageObjects<\/a><br \/>\n2. <a href=\"http:\/\/www.ralphlavelle.net\/2012\/08\/the-page-object-pattern-for-ui-tests.html\">http:\/\/www.ralphlavelle.net\/2012\/08\/the-page-object-pattern-for-ui-tests.html<\/a><\/p>\n<hr>\n<h3>Hire testing consultants for your startup from Qxf2<\/h3>\n<p>This article is pretty dated but as you can see, even in 2014, we were working on a test automation framework that is now used by over 150 companies. Qxf2 engineers were technical back then and we continue to evolve. If you are looking to work with technical testing consultants that can make an immediate impact, browse our many <a href=\"https:\/\/qxf2.com\/?utm_source=page-object-model-selenium-python&#038;utm_medium=click&#038;utm_campaign=From%20blog\">outstanding QA service offerings for startups<\/a>. If you find one of our tailored services interests you, reach out to get the ball rolling!<\/p>\n<hr>\n<script>(function() {\n\twindow.mc4wp = window.mc4wp || {\n\t\tlisteners: [],\n\t\tforms: {\n\t\t\ton: function(evt, cb) {\n\t\t\t\twindow.mc4wp.listeners.push(\n\t\t\t\t\t{\n\t\t\t\t\t\tevent   : evt,\n\t\t\t\t\t\tcallback: cb\n\t\t\t\t\t}\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n})();\n<\/script><!-- Mailchimp for WordPress v4.10.1 - https:\/\/wordpress.org\/plugins\/mailchimp-for-wp\/ --><form id=\"mc4wp-form-1\" class=\"mc4wp-form mc4wp-form-6165 mc4wp-form-theme mc4wp-form-theme-blue\" method=\"post\" data-id=\"6165\" data-name=\"Newsletter\" ><div class=\"mc4wp-form-fields\"><div style=\"border:3px; border-style:dashed;border-color:#56d1e1;padding:1.2em;\">\r\n  <h1 style=\"text-align: center; padding-top: 20px; padding-bottom: 20px; color: #592b1b;\">Subscribe to our weekly Newsletter<\/h1>\r\n  <input style=\"margin: auto;\" type=\"email\" name=\"EMAIL\" placeholder=\"Your email address\" required \/>\r\n  <br>\r\n  <p style=\"text-align: center;\">\r\n    <input style=\"background-color: #890c06 !important; border-color: #890c06;\" type=\"submit\" value=\"Sign up\" \/>\r\n    \r\n  <\/p>\r\n  <p style=\"text-align: center;\">\r\n    <a href=\"http:\/\/mailchi.mp\/c9c7b81ddf13\/the-informed-testers-newsletter-20-oct-2017\"><small>View a sample<\/small><\/a>\r\n  <\/p>\r\n  <br>\r\n<\/div><\/div><label style=\"display: none !important;\">Leave this field empty if you're human: <input type=\"text\" name=\"_mc4wp_honeypot\" value=\"\" tabindex=\"-1\" autocomplete=\"off\" \/><\/label><input type=\"hidden\" name=\"_mc4wp_timestamp\" value=\"1776627485\" \/><input type=\"hidden\" name=\"_mc4wp_form_id\" value=\"6165\" \/><input type=\"hidden\" name=\"_mc4wp_form_element_id\" value=\"mc4wp-form-1\" \/><div class=\"mc4wp-response\"><\/div><\/form><!-- \/ Mailchimp for WordPress Plugin -->\n<hr>\n","protected":false},"excerpt":{"rendered":"<p>UPDATE: We are retiring this post in favor of a newer post: Page Object Model (Selenium, Python) Please refer to the newer post. It has a more detailed architectural breakdown, provides many more code snippets and write an automated test for a very relatable application &#8211; Gmail. You could also visit our open-sourced Python + Selenium test automation framework based [&hellip;]<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[63,18,30],"tags":[],"class_list":["post-699","post","type-post","status-publish","format-standard","hentry","category-page-object-model","category-python","category-selenium"],"_links":{"self":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/699","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/comments?post=699"}],"version-history":[{"count":54,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/699\/revisions"}],"predecessor-version":[{"id":22913,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/posts\/699\/revisions\/22913"}],"wp:attachment":[{"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/media?parent=699"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/categories?post=699"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/qxf2.com\/blog\/wp-json\/wp\/v2\/tags?post=699"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}