[Перевод] Jest и Puppeteer: автоматизация тестирования веб-интерфейсов

Эту статью написал программист из Италии Валентино Гаглиарди. Он говорит, что сразу после выхода Puppeteer его заинтересовала автоматизация тестирования веб-интерфейсов с использованием данной библиотеки и Jest. После этого он приступил к экспериментам.

tgwylackafqptsmzpztufu_13qy.jpeg

Здесь речь пойдёт об основах работы с Puppeteer и Jest на примере тестирования веб-формы. Также тут будут рассмотрены особенности использования Chromium с пользовательским интерфейсом и без него, и некоторые полезные мелочи, касающиеся различных аспектов тестирования веб-страниц и организации рабочей среды. Валентино полагает, что, хотя Puppeteer — инструмент сравнительно новый и его API вполне может подвергаться изменениям, у него есть шанс занять достойное место в арсенале веб-разработчиков.

О некоторых особенностях рассматриваемых тестов


Недавно я писал тесты интерфейсов и в это время наткнулся на пост Кента С. Доддса, посвящённый повышению стабильности тестов за счёт использования атрибута data-*. Это, если в двух словах, пользовательские атрибуты, которые можно задавать для практически любых HTML-элементов. Они особенно полезны при организации обмена данными с JavaScript-программами.

Материал Кента попался мне очень вовремя, так как я тогда пользовался примерно такими конструкциями:

await page.waitForSelector("#contact-form");
await page.click("#name");
await page.type("#name", user.name);


Тут надо отметить, что я, в основном, занимаюсь серверным программированием. И хотя я пока не агитирую за использование data-* в тестах, я должен признать, что это, всё-таки, отличный подход. Особенно полезно это в крупных приложениях, но пока, в нашем простом примере, я будут использовать классический способ обращения к элементам.

Тестирование формы обратной связи


Итак, наша цель заключается в тестировании формы обратной связи на данной странице, работой над которой я занимаюсь. Вот эта форма:

833fae5309b6c76018e6b267405f6b7a.png


Она включает в себя следующие элементы:

  • Поле для ввода имени.
  • Поле для ввода адреса электронной почты.
  • Поле для ввода телефона.
  • Область для ввода произвольного текста.
  • Флажок, устанавливая который, пользователь соглашается с правилами обработки данных.
  • Кнопка отправки формы.


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

Настройка проекта


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

8bd698219b68cccfa5db9f9f81611111.png


Jest — фреймворк для тестирования, разработанный Facebook. Jest даёт платформу для автоматизированного тестирования, а также базовую библиотеку, позволяющую строить утверждения (Expect).

Puppeteer —  библиотека для Node.js, которая позволяет управлять браузером Chromium без пользовательского интерфейса. Инструмент это довольно новый, поэтому самое время его опробовать и подумать над тем, нужен ли он в конкретном проекте, и если нужен — о том, как встроить его в существующую экосистему.

Faker — библиотека для Node.js, которая умеет генерировать случайные данные. Среди них — имена, телефоны, адреса. Это, кстати, нечто вроде Faker для PHP.

Если у вас уже есть проект, на котором вы хотите поэкспериментировать, установить необходимые библиотеки можно такой командой:

npm i jest puppeteer faker --save-dev


Установка Puppeteer займёт некоторое время, так как, кроме прочего, в ходе установки библиотеки устанавливается и браузер Chromium.

Chromium — это веб-браузер с открытым исходным кодом, который является основой Google Chrome. Chromium и Chrome имеют практически одинаковые возможности, основные отличия заключаются в особенностях лицензирования.

После того, как всё необходимое будет установлено, настроим Jest в package.json. Команда test должна указывать на исполняемый файл Jest:

"scripts": {
  "test": "jest"
}


Кроме того, в Jest я предпочитаю пользоваться такой конструкцией:

import puppeteer from "puppeteer";


Поэтому тут нам понадобится Babel для Jest:

npm i babel-core babel-jest babel-preset-env --save-dev


После установки Babel создадим в папке проекта файл .babelrc со следующим содержимым:

{
  "presets": ["env"]
}


На этом предварительная подготовка завершена и мы можем приступать к написанию тестов.

Пишем тесты


Создадим новую директорию в папке проекта. Назвать её можно test или spec. Затем, в этой директории, надо создать файл form.spec.js.

Теперь предлагаю рассмотреть код тестов по частям, начав с секции импорта. Ниже я приведу весь этот код целиком.

Сначала импортируем Faker и Puppeteer:

import faker from "faker";
import puppeteer from "puppeteer";


Теперь зададим URL формы. Возможно, вы решите протестировать версию формы, которая находится в разработке и доступна локально, вместо того, чтобы подключаться к рабочему сайту:

const APP = "https://www.change-this-to-your-website.com/contact-form.html"


Теперь, с помощью Faker, создаём фиктивного пользователя:

const lead = {
  name: faker.name.firstName(),
  email: faker.internet.email(),
  phone: faker.phone.phoneNumber(),
  message: faker.random.words()
};


Дальше — определяем некоторые переменные, необходимые для работы с Puppeteer:

let page;
let browser;
const width = 1920;
const height = 1080;


Теперь настраиваем поведение Puppeteer:

beforeAll(async () => {
  browser = await puppeteer.launch({
    headless: false,
    slowMo: 80,
    args: [`--window-size=${width},${height}`]
  });
  page = await browser.newPage();
  await page.setViewport({ width, height });
});
afterAll(() => {
  browser.close();
});


Здесь мы пользуемся методами Jest beforeAll и afterAll. Первый нам нужен из-за того, что перед выполнением тестов требуется запустить, с помощью Puppeteer, браузер. После запуска браузера мы можем открыть новую страницу. Когда тесты завершатся, браузер должен быть закрыт. Делается это в методе afterAll с помощью команды browser.close().

Надо отметить, что мы не ограничены лишь методами beforeAll и afterAll. Для того, чтобы узнать о других возможностях Jest, взгляните на документацию к этой библиотеке. В любом случае, рекомендуется пользоваться одним экземпляром браузера для выполнения всего набора тестов, вместо того, чтобы открывать и закрывать браузер для каждого отдельного теста.

Тут мне хотелось бы сделать некоторые комментарии по поводу вышеприведённого фрагмента кода. А именно, обратите внимание на то, что я запускаю браузер в оконном режиме, используя параметр headless: false. В данном случае так сделано для того, чтобы иметь возможность записать происходящее на экране на видео и показать процесс тестирования. Выполняя реальные тесты с помощью описываемых инструментов обычно незачем наблюдать за тем, что происходит. Для того, чтобы браузер запускался без интерфейса, можно просто убрать параметры, используемые при вызове метода launch().

То же самое касается и команды setViewPort(), которую тоже можно убрать. Или, что даже лучше, можно настроить два разных окружения тестирования. Одно использовать для визуальной отладки (речь об этом пойдёт ниже), второе — для работы с браузером без пользовательского интерфейса.

Теперь пишем код тестов:

describe("Contact form", () => {
  test("lead can submit a contact request", async () => {
    await page.waitForSelector("[data-test=contact-form]");
    await page.click("input[name=name]");
    await page.type("input[name=name]", lead.name);
    await page.click("input[name=email]");
    await page.type("input[name=email]", lead.email);
    await page.click("input[name=tel]");
    await page.type("input[name=tel]", lead.phone);
    await page.click("textarea[name=message]");
    await page.type("textarea[name=message]", lead.message);
    await page.click("input[type=checkbox]");
    await page.click("button[type=submit]");
    await page.waitForSelector(".modal");
  }, 16000);
});


Обратите внимание на возможность использования конструкции async/await с Jest. Тут предполагается, что тестирование проводится с использованием одной из свежих версий Node.js.

Рассмотрим эти тесты. Вот какие действия выполняет браузер, управляемый программно:

  • Переход по адресу, заданному в константе APP.
  • Ожидание появления формы обратной связи.
  • Щелчки по полям и заполнение их данными.
  • Установка флажка.
  • Отправка формы.
  • Ожидание появления модального окна.


Обратите внимание на то, что функции Jasmine test(), в качестве второго параметра, передан тайм-аут (16000). Это позволяет наблюдать за тем, как именно браузер работает со страницей.

Если выполнять тестирование с использованием браузера, видимого на экране, и не задать при этом тайм-аут, возникнет следующая ошибка:

Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL


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

Теперь всё готово и тестирование можно запустить следующей командой:

npm test


После этого остаётся лишь наблюдать за браузером, который сам работает со страницей.


Если кому интересно, это экранное видео было записано в Fedora с помощью recordmydesktop и такой команды:

recordmydesktop --width 1024 --height 768 -x 450 -y 130 --no-sound


Однако, это ещё не всё.

Тестирование других элементов интерфейса


Теперь, когда с формой мы разобрались, можно протестировать ещё какие-нибудь элементы страницы.

Выясним, как обстоят дела с тем, что находится в теге . Как известно, там должен быть осмысленный заголовок страницы:

describe("Testing the frontend", () => {
  test("assert that  is correct", async () => {
    const title = await page.title();
    expect(title).toBe(
      "Gestione Server Dedicati | Full Managed | Assistenza Sistemistica"
    );
  });
  // Сюда можно добавить ещё тестов
});</code></pre>

<p><br />
А что у нас с навигационной панелью? Она должна присутствовать на странице. Проверим, с помощью Jest и Puppeteer, так ли это: </p>

<pre><code>//
  test("assert that a div named navbar exists", async () => {
    const navbar = await page.$eval(".navbar", el => (el ? true : false));
    expect(navbar).toBe(true);
  });
//</code></pre>

<p><br />
Ещё можно выяснить, содержит ли некий элемент тот текст, который в нём должен быть: </p>

<pre><code>//
  test("assert that main title contains the correct text", async () => {
    const mainTitleText = await page.$eval("[data-test=main-title]", el => el.textContent);
    expect(mainTitleText).toEqual("GESTIONE SERVER, Full Managed");
  });
//</code></pre>

<p><br />
А как насчёт испытаний страницы на предмет поисковой оптимизации? Проверим, например, наличие канонической ссылки: </p>

<pre><code>describe("SEO", () => {
  test("canonical must be present", async () => {
    await page.goto(`${APP}`);
    const canonical = await page.$eval("link[rel=canonical]", el => el.href);
    expect(canonical).toEqual("https://www.servermanaged.it/");
  });
});</code></pre>

<p><br />
По тем же принципам можно создать множество других тестов.</p>

<p>В итоге все мои тесты успешно завершились, о чём можно судить по приятным сообщениям зелёного цвета.</p>

<div><img src="https://habrastorage.org/getpro/habr/post_images/3dd/065/8ec/3dd0658ecab0d51c8b396d29b2f5553a.png" alt="3dd0658ecab0d51c8b396d29b2f5553a.png" /></div>

<p><br /></p>

<h2><span>Визуальная отладка</span></h2>

<p><br />
Мы уже говорили о том, что Puppeteer позволяет автоматизировать работу с Chromium, запускаемым с пользовательским интерфейсом или без него. Вспомним следующий фрагмент кода: </p>

<pre><code>beforeAll(async () => {
  browser = await puppeteer.launch({
      // Режим отладки
      headless: false,
      slowMo: 80,
      args: [`--window-size=1920,1080`]
    });
  page = await browser.newPage();
///
});</code></pre>

<p><br />
Кроме того, нужно помнить о том, что запуская браузер с графическим интерфейсом, надо передать Jasmine параметр тайм-аута. В противном случае тестирование завершится быстро и неожиданно. Тайм-аут — это второй аргумент метода <code>test()</code>: </p>

<pre><code>describe("Contact form", () => {
  test(
    "lead can submit a contact request",
    async () => {
    ///// утверждения
    },
    16000 // <<< Тайм-аут Jasmine
  );
});</code></pre>

<p><br />
При автоматизированном тестировании выводить окно браузера не нужно, иначе выполнение хоть сколько-нибудь серьёзного тестового набора займёт целую вечность. Однако, иногда полезно понаблюдать за тем, что именно происходит в браузере, управляемом из кода. Как наладить удобное переключение между тестами с использованием браузера с интерфейсом и без него? </p>

<p>Решить эту задачу можно, создав вспомогательную функцию. Сделаем такую и поместим её в файл <code>testingInit.js</code>: </p>

<pre><code>export const isDebugging = () => {
  let debugging_mode = {
    puppeteer: {
      headless: false,
      slowMo: 80,
      args: [`--window-size=1920,1080`]
    },
    jasmine: 16000
  };
  return process.env.NODE_ENV === "debug" ? debugging_mode : false;
};</code></pre>

<p><br />
Затем к ней можно обратиться из файла с кодом теста, сначала импортировав её, а потом воспользовавшись ей при запуске браузера: </p>

<pre><code>///
import { isDebugging } from "./testingInit.js";
///
beforeAll(async () => {
  browser = await puppeteer.launch(isDebugging().puppeteer)); // <<< Визуальный режим
  page = await browser.newPage();
///
});</code></pre>

<p><br />
Та же функция пригодится и при настройке тайм-аута: </p>

<pre><code>describe("Contact form", () => {
  test(
    "lead can submit a contact request",
    async () => {
    ///// утверждения
    }, isDebugging().jasmine // <<< тайм-аут Jasmine
  );
});</code></pre>

<p><br />
После этого, для того, чтобы начать тестирование в браузере без интерфейса, достаточно выполнить такую команду: </p>

<pre><code>npm test</code></pre>

<p><br />
Для запуска тестов в визуальном режиме надо будет сделать следующее: </p>

<pre><code>NODE_ENV=debug npm test</code></pre>

<p><br /></p>

<h2><span>Итоги</span></h2>

<p><br />
Возможно, вам пока не вполне удобно работать с самими Puppeteer или его API. Я вас понимаю. И если новизна этого проекта наполняет вас сомнением в его практической применимости, вы можете взглянуть, например, на Cypress. Однако, Puppeteer даёт разработчикам поистине безграничные возможности. Сейчас создаются тестировочные фреймворки на основе этой библиотеки. Конечно, со временем API Puppeteer может и поменяться, но полагаю, что базовые вещи, о которых мы тут говорили, никуда не денутся. Кроме того, нельзя не отметить, что Puppeteer отлично сочетается с Jest. Многие, кроме того, выполняют с помощью Puppeteer E2E-тестирование.</p>

<p>Уважаемые читатели! Как вы автоматизируете тестирование веб-интерфейсов? </p>
    
            <p class="copyrights"><span class="source">© <a target="_blank" rel="nofollow" href="https://habrahabr.ru/post/342578/">Habrahabr.ru</a></span></p>
                    </div>
                                                    
            <br>
            <!--<div align="left">
                <script type="text/topadvert">
                load_event: page_load
                feed_id: 12105
                pattern_id: 8187
                tech_model:
                </script><script type="text/javascript" charset="utf-8" defer="defer" async="async" src="//loader.topadvert.ru/load.js"></script>
            </div>
            <br>-->

            <div style="padding-left: 20px;">
                <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2514821055276660"
                        crossorigin="anonymous"></script>
                <!-- PCNews 336x280 -->
                <ins class="adsbygoogle"
                     style="display:block"
                     data-ad-client="ca-pub-2514821055276660"
                     data-ad-slot="1200562049"
                     data-ad-format="auto"></ins>
                <script>
                    (adsbygoogle = window.adsbygoogle || []).push({});
                </script>
            </div>
            <!-- comments -->
                            <noindex>
                    <div style="margin: 25px;" id="disqus_thread"></div>
                    <script type="text/javascript">
                        var disqus_shortname = 'pcnewsru';
                        var disqus_identifier = '802436';
                        var disqus_title = '[Перевод] Jest и Puppeteer: автоматизация тестирования веб-интерфейсов';
                        var disqus_url = 'http://pcnews.ru/blogs/[perevod]_jest_i_puppeteer_avtomatizacia_testirovania_veb_interfejsov-802436.html';

                        (function() {
                            var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
                            dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
                            (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
                        })();
                    </script>
                    <!--<noscript>Please enable JavaScript to view the <a rel="nofollow" href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>-->
                    <!--<a href="http://disqus.com" rel="nofollow" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>-->
                </noindex>
            
        </div>

        <br class="clearer"/>
    </div>
    <br class="clearer"/>

    

        <div id="footer-2nd"></div>

        <div id="footer">
            <br/><br/>
            <ul class="horz-menu">
                <li class="about"><a href="/info/about.html" title="О проекте">О
                        проекте</a></li>
                <li class="additional-menu"><a href="/archive.html" title="Архив материалов">Архив</a>
                </li>
                <li class="additional-menu"><a href="/info/reklama.html"
                                               title="Реклама" class="menu-item"><strong>Реклама</strong></a>
                    <a href="/info/partners.html" title="Партнёры"
                       class="menu-item">Партнёры</a>
                    <a href="/info/legal.html" title="Правовая информация"
                       class="menu-item">Правовая информация</a>
                    <a href="/info/contacts.html" title="Контакты"
                       class="menu-item">Контакты</a>
                    <a href="/feedback.html" title="Обратная связь" class="menu-item">Обратная
                        связь</a></li>
                <li class="email"><a href="mailto:pcnews@pcnews.ru" title="Пишите нам на pcnews@pcnews.ru"><img
                                src="/media/i/email.gif" alt="e-mail"/></a></li>
                <li style="visibility: hidden">
                    <noindex>
                        <!-- Rating@Mail.ru counter -->
                        <script type="text/javascript">
                            var _tmr = window._tmr || (window._tmr = []);
                            _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()});
                            (function (d, w, id) {
                                if (d.getElementById(id)) return;
                                var ts = d.createElement("script");
                                ts.type = "text/javascript";
                                ts.async = true;
                                ts.id = id;
                                ts.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//top-fwz1.mail.ru/js/code.js";
                                var f = function () {
                                    var s = d.getElementsByTagName("script")[0];
                                    s.parentNode.insertBefore(ts, s);
                                };
                                if (w.opera == "[object Opera]") {
                                    d.addEventListener("DOMContentLoaded", f, false);
                                } else {
                                    f();
                                }
                            })(document, window, "topmailru-code");
                        </script>
                        <noscript>
                            <div style="position:absolute;left:-10000px;">
                                <img src="//top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;" height="1"
                                     width="1" alt="Рейтинг@Mail.ru"/>
                            </div>
                        </noscript>
                        <!-- //Rating@Mail.ru counter -->

                    </noindex>
                </li>
            </ul>
        </div>

        <!--[if lte IE 7]>
        <iframe id="popup-iframe" frameborder="0" scrolling="no"></iframe>
        <![endif]-->
        <!--<div id="robot-image"><img class="rbimg" src="i/robot-img.png" alt="" width="182" height="305" /></div>-->
        <!--[if IE 6]>
        <script>DD_belatedPNG.fix('#robot-image, .rbimg');</script><![endif]-->

    </div>

<!--[if lte IE 7]>
<iframe id="ie-popup-iframe" frameborder="0" scrolling="no"></iframe>
<![endif]-->


    <div id="footer-adlinks"></div>

    
    
    
        <noindex>


            <!--LiveInternet counter--><script type="text/javascript">
                document.write("<a rel='nofollow' href='//www.liveinternet.ru/click' "+
                    "target=_blank><img src='//counter.yadro.ru/hit?t45.6;r"+
                    escape(document.referrer)+((typeof(screen)=="undefined")?"":
                        ";s"+screen.width+"*"+screen.height+"*"+(screen.colorDepth?
                            screen.colorDepth:screen.pixelDepth))+";u"+escape(document.URL)+
                    ";"+Math.random()+
                    "' alt='' title='LiveInternet' "+
                    "border='0' width='1' height='1'><\/a>")
            </script><!--/LiveInternet-->

            <!-- Rating@Mail.ru counter -->
            <script type="text/javascript">
                var _tmr = window._tmr || (window._tmr = []);
                _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()});
                (function (d, w, id) {
                    if (d.getElementById(id)) return;
                    var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id;
                    ts.src = "https://top-fwz1.mail.ru/js/code.js";
                    var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);};
                    if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); }
                })(document, window, "topmailru-code");
            </script><noscript><div>
                    <img src="https://top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;position:absolute;left:-9999px;" alt="Top.Mail.Ru" />
                </div></noscript>
            <!-- //Rating@Mail.ru counter -->



            <!-- Yandex.Metrika counter -->
            <script type="text/javascript">
                (function (d, w, c) {
                    (w[c] = w[c] || []).push(function () {
                        try {
                            w.yaCounter23235610 = new Ya.Metrika({
                                id: 23235610,
                                clickmap: true,
                                trackLinks: true,
                                accurateTrackBounce: true,
                                webvisor: true,
                                trackHash: true
                            });
                        } catch (e) {
                        }
                    });

                    var n = d.getElementsByTagName("script")[0],
                        s = d.createElement("script"),
                        f = function () {
                            n.parentNode.insertBefore(s, n);
                        };
                    s.type = "text/javascript";
                    s.async = true;
                    s.src = "https://mc.yandex.ru/metrika/watch.js";

                    if (w.opera == "[object Opera]") {
                        d.addEventListener("DOMContentLoaded", f, false);
                    } else {
                        f();
                    }
                })(document, window, "yandex_metrika_callbacks");
            </script>
            <noscript>
                <div><img src="https://mc.yandex.ru/watch/23235610" style="position:absolute; left:-9999px;" alt=""/>
                </div>
            </noscript>
            <!-- /Yandex.Metrika counter -->

            <!-- Default Statcounter code for PCNews.ru http://pcnews.ru-->
            <script type="text/javascript">
                var sc_project=9446204;
                var sc_invisible=1;
                var sc_security="14d6509a";
            </script>
            <script type="text/javascript"
                    src="https://www.statcounter.com/counter/counter.js"
                    async></script>
            <!-- End of Statcounter Code -->

            <script>
                (function (i, s, o, g, r, a, m) {
                    i['GoogleAnalyticsObject'] = r;
                    i[r] = i[r] || function () {
                            (i[r].q = i[r].q || []).push(arguments)
                        }, i[r].l = 1 * new Date();
                    a = s.createElement(o),
                        m = s.getElementsByTagName(o)[0];
                    a.async = 1;
                    a.src = g;
                    m.parentNode.insertBefore(a, m)
                })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

                ga('create', 'UA-46280051-1', 'pcnews.ru');
                ga('send', 'pageview');

            </script>

            <script async="async" src="/assets/uptolike.js?pid=49295"></script>

        </noindex>
    



<!--<div id="AdwolfBanner40x200_842695" ></div>-->
<!--AdWolf Asynchronous Code Start -->

<script type="text/javascript" src="https://pcnews.ru/js/blockAdblock.js"></script>

<script type="text/javascript" src="/assets/jquery.min.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.json.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.form.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.easing.1.2.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/jquery/effects.core.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/browser-sniff.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/scripts.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-utils.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-auth.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-fiximg.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-infobox.js"></script>
</body>
</html>