Pull to refresh
2723.05
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Обработка окон и вкладок браузера в Selenium PHP

Reading time15 min
Views9.4K
Original author: Himanshu Sheth

Один из распространённых сценариев работы с веб-приложениями заключается в открывании нового окна (или вкладки) браузера после выполнения пользователем определённого действия. Многие веб-разработчики используют HTML-тэг __blank, приказывающий браузеру при нажатии на ссылку открыть новое окно (или вкладку, это зависит от выбранных пользователем настроек). Работа с окнами в Selenium при помощи PHP может использоваться для автоматизации взаимодействия с окнами, вкладками и даже всплывающими окнами браузера.

Всплывающие окна могут иногда раздражать посетителей веб-сайта, однако зачастую у разработчика нет иного выхода, кроме как использовать их. Как Selenium различает окна и вкладки? Как можно использовать автоматизацию для беспроблемного переключения между окнами или вкладками браузера? Как реализовать работу с окнами в Selenium?

В этой статье мы подробно рассмотрим применение Selenium для автоматизации взаимодействия с браузерами, вкладками и всплывающими окнами. Для демонстрации работы с окнами в Selenium PHP мы воспользуемся PHPUnit — фреймворком юнит-тестирования для PHP.

Что такое дескриптор окна?


Дескриптор окна (window handle) — это уникальный идентификатор, основная задача которого заключается в хранении адресов всех окон. При создании экземпляра Selenium WebDriver окну назначается алфавитно-цифровой ID. Этот уникальный ID называется дескриптором окна — указателем на окно, позволяющим идентифицировать окно браузера.

Для каждого окна/вкладки/всплывающего окна дескриптор окна (или ID) уникален. Для переключения между окнами (или вкладками) Selenium WebDriver использует функции дескрипторов окон в Selenium PHP.

Уникальный ID сохраняется до закрытия сессии Selenium WebDriver (выполняемого через API WebDriver.Quit или WebDriver.Close). Функции дескрипторов окон используются для получения подробной информации о дескрипторах всех окон. Фундаментальные основы работы с окнами в Selenium остаются неизменными, вне зависимости от того, используете ли вы Selenium с PHP или Selenium с другими языками программирования (например, Python, Java, и т. п.).

Вот некоторые из самых распространённых сценариев, в которых приходится иметь дело с множественными окнами (или вкладками):

  • Формы, требующие выбора даты в новом открытом окне
  • Нажатия кнопок, открывающие новую вкладку (или окно)
  • Всплывающие окна, используемые для показа конечному пользователю каких-либо предложений; эту стратегию часто используют порталы с вакансиями
  • Работа с окнами, отображающими рекламу

Пример сценария, в котором нажатие на родительское (или базовое) окно открывает два «дочерних окна». Каждое из них имеет уникальный дескриптор окна, и этот дескриптор хранится, пока окна не будут уничтожены (т. е. закрыты). Общее количество окон становится равным трём (parent + child-1 + child-2). При нажатии кнопки/ссылки в «Child-1» и «Child-2», соответственно, откроется окно «Grand Child-1» и «Grand Child-2». После этого суммарно будет открыто пять окон, и каждое из них будет иметь уникальный дескриптор окна, который можно использовать для автоматизации операций с окном.

Команды для работы с окнами в Selenium с PHP


Selenium предоставляет различные методы для работы с несколькими окнами. Вы можете также изучить наше руководство по работе с несколькими окнами при помощи Selenium и Protractor. Ниже представлены самые часто используемые при тестировании Selenium PHP команды для переключения окон браузера и работы со всплывающими окнами.

Если вы специалист по PHP, то вы можете получить сертификат специализации в программировании на базовом PHP и расширить свои возможности для развития своей карьеры в сфере автоматического тестирования PHP.

SwitchTo Window


Команда SwitchTo используется для переключения фокуса на новое окно или вкладку браузера. Для переключения фокуса на новое окно в качестве аргумента команде передаётся дескриптор окна соответствующего окна браузера.

/* $wHandle - это дескриптор окна (или ID), на которое нужно выполнить переключение */
$this->webDriver->switchTo()->window($wHandle);
 
/* Переключение также можно выполнять получением количества окон при помощи getWindowHandles или getWindowHandle  */
$this->webDriver->switchTo()->window($HandleCount[win-number]);

getWindowHandle


Метод getWindowHandle в Selenium PHP возвращает Window ID (уникальный алфавитно-цифровой идентификатор окна) для текущего активного (или находящегося в фокусе) окна.

$wHandle = $this->webDriver->getWindowHandle();

В показанном выше примере $wHandle — это ID окна, полученный при помощи метода getWindowHandle.

getWindowHandles


Это важный метод для работы с окнами в Selenium с PHP. Метод getWindowHandles возвращает множество дескрипторов окон (и вкладок), открытых одним экземпляром драйвера, в том числе родительских и дочерних окон. Например, если операция «щелчок на кнопке» в родительском окне открывает новую вкладку, то метод getWindowHandles вернёт дескрипторы родительского и дочернего окна (например, вкладки). Если затем к множеству, возвращённому методом getWindowHandles, применить оператор sizeof, то он вернёт размер множества (в данном случае 2).

Аналогично, если открытая в родительском окне веб-страница открывает 8 всплывающих окон, то количество дескрипторов, полученных применением оператора sizeof для множества, возвращённого getWindowHandles, будет 9.

Каждое из окон будет иметь уникальный дескриптор окна (или идентификатор) для удобства идентификации окна.

/* В случае веб-страницы, открывающей новую вкладку, getWindowHandles возвращает массив, состоящий из двух дескрипторов окон (т. е. дескрипторов родительского и дочернего окон) */

$HandleCount = $this->webDriver->getWindowHandles();

 

/* Возвращает размер массива дескрипторов окон. В данном примере он будет равен 2 */

echo ("\n Total number of window handles are " . sizeof($HandleCount));

 

/* Печать дескриптора окна в родительском окне */

echo ("\n Window 0: " . $HandleCount[0]);

 

/* Печать дескриптора окна в дочернем окне */

echo ("\n Window 0: " . $HandleCount[1]);


Как работать с несколькими окнами, вкладками и всплывающими окнами в Selenium PHP


Мы продемонстрируем сценарии тестов работы с окнами в Selenium PHP при помощи PHPUnit в облачном Selenium Grid сервиса LambdaTest. Кросс-браузерное тестирование при помощи PHPUnit в облачном Selenium Grid помогает в тестировании на различных сочетаниях браузеров, платформ и эмуляторов устройств.

Для начала создадим аккаунт LambdaTest и обратим внимание, что user-name и access-key доступны на странице профиля. Тесты выполняются на сочетании Chrome 85.0 + Windows 10. Возможности браузера сгенерированы при помощи генератора возможностей LambdaTest.

Для установки фреймворка PHPUnit мы создаём для этого проекта файл composer.json:

{
   "require":{
      "php":">=7.1",
      "phpunit/phpunit":"^9",
      "phpunit/phpunit-selenium": "*",
      "php-webdriver/webdriver":"1.8.0",
      "symfony/symfony":"4.4",
      "brianium/paratest": "dev-master"
   }
}

Введём команду composer require и дважды нажмём на кнопку «Enter», чтобы приступить к установке фреймворка PHPUnit. После завершения фреймворк PHPUnit (версии 9.3) будет установлен.

Файл composer.lock содержит информацию о зависимостях, а в папке vendor содержатся все зависимости.


Файл vendor/autoload.php будет использоваться в коде тестов, чтобы классы (и их методы), предоставленные этими библиотеками, можно было использовать в реализации.

Теперь настало время продемонстрировать различные сценарии работы с окнами в Selenium с PHP.

▍ Работа с несколькими окнами браузера в Selenium PHP


Для демонстрации работы с несколькими окнами в Selenium PHP мы используем следующий тестовый сценарий:

  1. Открываем страницу LambdaTest в браузере Chrome.
  2. Получаем дескриптор текущего окна в фокусе.
  3. Открываем страницу блога LambdaTest в новом окне при помощи HTML-атрибута __blank.
  4. Печатаем соответствующие дескрипторы окон.
  5. Переключаемся на окно, где открыт второй URL.
  6. Добавляем утверждение на случай, если заголовок окна не соответствует ожидаемому заголовку.
  7. Закрываем окно в фокусе.
  8. Переключаемся на первое окно и добавляем утверждение на случай, если заголовок окна не соответствует ожидаемому заголовку.
  9. Закрываем окно браузера.


❒ Реализация


<?php
require 'vendor/autoload.php';
 
use PHPUnit\Framework\TestCase;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
 
$GLOBALS['LT_USERNAME'] = "user-name";
# accessKey:  AccessKey можно сгенерировать в панели автоматизации или в разделе профиля
$GLOBALS['LT_APPKEY'] = "access-key";
 
class WindowSwitchTest extends TestCase
{
  protected $webDriver;
 
  public function build_browser_capabilities(){
    /* $capabilities = DesiredCapabilities::chrome(); */
    $capabilities = array(
      "build" => "[PHP] Window Switching with Chrome on Windows 10",
      "name" => "[PHP] Window Switching with Chrome on Windows 10",
      "platform" => "Windows 10",
      "browserName" => "Chrome",
      "version" => "85.0"
    );
    return $capabilities;
  }
  
  public function setUp(): void
  {
    $url = "https://". $GLOBALS['LT_USERNAME'] .":" . $GLOBALS['LT_APPKEY'] ."@hub.lambdatest.com/wd/hub";
    $capabilities = $this->build_browser_capabilities();
    /* Скачать Selenium Server 3.141.59 с 
    https://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar
    */
    /* $this->webDriver = RemoteWebDriver::create('http://localhost:4444/wd/hub', $capabilities); */
    $this->webDriver = RemoteWebDriver::create($url, $capabilities);
  }
 
  public function tearDown(): void
  {
    $this->webDriver->quit();
  }
  /*
  * @test
  */ 
  public function test_SwitchToNewWindow()
  {
    $test_url_1 = "https://www.lambdatest.com";
    $title_1 = "Most Powerful Cross Browser Testing Tool Online | LambdaTest";
 
    $test_url_2 = "https://www.lambdatest.com/blog/";
    $title_2 = "LambdaTest | A Cross Browser Testing Blog";
 
    $this->webDriver->get($test_url_1);
    $this->webDriver->manage()->window()->maximize();
 
    $wHandle = $this->webDriver->getWindowHandle();
    /* echo ("\n Primary Window Handle is " . $wHandle ); */
    sleep(5);
 
    /* Открываем второе окно */
    /* $link = "window.open('https://www.lambdatest.com/blog/', '_blank', 'toolbar=yes,scrollbars=yes,resizable=yes,width=800,height=800')"; */
    $link = "window.open('". $test_url_2 ."', '_blank', 'toolbar=yes,scrollbars=yes,resizable=yes,width=1200,height=1200')";
    $this->webDriver->executeScript($link);
    /* $this->webDriver->manage()->window()->maximize(); */
 
    /* Фокус теперь на втором окне */
    /* Количество дескрипторов будет равным двум */  
    $HandleCount = $this->webDriver->getWindowHandles();
    echo ("\n Total number of window handles are " . sizeof($HandleCount));
    echo ("\n Window 0: " . $HandleCount[0]);
    echo ("\n Window 1: " . $HandleCount[1]);
 
    sleep(10);
 
    /* Создаём утверждение на случай, если количество окон не равно 2 */
    $this->assertEquals(2, sizeof($HandleCount));
 
    /* Проверяем соответствие заголовков окон */
    $this->webDriver->switchTo()->window($HandleCount[1]);
    $win_title_2 = $this->webDriver->getTitle();
    echo ("\n Title of the window 1 is " . $win_title_2);
    sleep(10);
    $this->assertEquals($win_title_2, $title_2);
 
    /* Закрываем новое открытое окно и возвращаемся к старому окну */
    $this->webDriver->close();
    sleep(10);
 
    /* Возвращаемся к окну с дескриптором = 0 */
    $this->webDriver->switchTo()->window($wHandle);
    /* Проверяем, совпадают ли заголовки окон */
    $win_title_1 = $this->webDriver->getTitle();
    echo ("\n Title of the window 0 is " . $win_title_1);
    $this->assertEquals($win_title_1, $title_1);
    sleep(10);
  }
}
?>

❒ Разбор кода


Существенная часть реализации в этой части туториала по Selenium PHP остаётся той же, которая была использована для работы с окнами в Selenium с PHP.

1. При открытиитестового URL для получения дескрипторов открытых окон браузера используется метод getWindowHandles WebDriver.

public function test_SwitchToNewTab()
{
      $test_url = "http://automationpractice.com/index.php";
      $title_1 = "My Store";
      $title_2 = "Selenium Framework - YouTube";
 
      ......................................
      ......................................
 
      $this->webDriver->get($test_url);
      $this->webDriver->manage()->window()->maximize();
      ......................................
      ......................................
      $HandleCount = $this->webDriver->getWindowHandles();

2. В конце страницы размещается веб-элемент с гиперссылкой на YouTube-канал сайта. При помощи метода executeScript, предоставленного JavaScriptExecutor, исполняется метод window.scrollTo JavaScript. Он выполняет переход к концу страницы.

$link = "window.scrollTo(0, document.body.scrollHeight)";
$this->webDriver->executeScript($link);

3. Для получения информации о требуемом веб-элементе мы используем расширение POM Builder для Chrome, чтобы получить информацию локатора (т. е. XPath).


Для получения информации о веб-элементе [with XPath – //a[contains(.,’Youtube’)] используется метод findElement класса WebDriverBy.

$browser_button = $this->webDriver->findElement(WebDriverBy::XPath("//a[contains(.,'Youtube')]"));

4. Для найденного веб-элемента вызывается метод click.

$browser_button->click();

5. Метод getWindowHandles возвращает алфавитно-цифровой массив (или множество), содержащий дескрипторы (или ID) текущих открытых окон (или вкладок) браузера. В нашем случае количество открытых окон будет равным 2.

Следовательно, после применения оператора sizeof к массиву (или множеству), возвращённому getWindowHandles, вернёт 2.

$HandleCount = $this->webDriver->getWindowHandles();
echo ("\n Total number of window handles are " . sizeof($HandleCount));
echo ("\n Window 0: " . $HandleCount[0]);
echo ("\n Window 1: " . $HandleCount[1]);

6. Для переключения на второе окно (т. е. $HandleCount[1]) используется метод switchTo Selenium WebDriver. Если заголовок окна не соответствует ожидаемому заголовку, срабатывает утверждение.

$this->webDriver->switchTo()->window($HandleCount[1]);
$win_title_2 = $this->webDriver->getTitle();
$this->assertEquals($win_title_2, $title_2);

7. Метод close закрывает окно в фокусе (т. е. вкладку, где открыт YouTube-канал).

$this->webDriver->close();

8. Выполняется переключение на родительское окно (т. е. $HandleCount[0]). Если заголовки не совпадают, срабатывает утверждение.

$this->webDriver->switchTo()->window($HandleCount[0]);
$win_title_1 = $this->webDriver->getTitle();
$this->assertEquals($win_title_1, $title_1);


❒ Исполнение


Вот дескрипторы двух окон браузера, созданных при тестировании.

Ниже показан снэпшот исполнения автоматизированного теста Selenium:


Как видно из снэпшота, дескрипторы родительского окна и вкладки уникальны. Когда окно и вкладка открыты, размер массива дескрипторов окон равен 2.


Теперь, когда мы увидели реализацию и исполнение работы с окнами и несколькими вкладками в Selenium с PHP, давайте подробнее узнаем о работе с несколькими всплывающими окнами браузера.

▍ Работа с несколькими всплывающими окнами браузера в Selenium с PHP


Для демонстрации работы со всплывающими окнами браузера в Selenium PHP мы используем следующий тестовый сценарий:

  1. Откроем www.popuptest.com/popuptest1.html в браузере Chrome.
  2. Закроем все всплывающие окна в обратном хронологическом порядке.
  3. Проверим, соответствует ли заголовок родительского окна ожидаемому заголовку.

Если тот же тест выполняется в локальном Selenium Grid, то нужно убедиться, что для Google Chrome включены всплывающие окна. Для включения всплывающих окон для Chrome на локальной машине перейдите по адресу chrome://settings/ -> Privacy and security -> Site Settings -> Pop-ups and redirects. Отключите опцию Block для www.popuptest.com:80.




❒ Реализация


<?php
require 'vendor/autoload.php';
 
use PHPUnit\Framework\TestCase;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
 
$GLOBALS['LT_USERNAME'] = "user-name";
# accessKey:  AccessKey можно сгенерировать в панели автоматизации или в разделе профиля
$GLOBALS['LT_APPKEY'] = "access-key";
 
class PopUpTest extends TestCase
{
  protected $webDriver;
 
  public function build_browser_capabilities(){
    /* $capabilities = DesiredCapabilities::chrome(); */
    $capabilities = array(
      "build" => "[PHP] Pop Up Testing with Chrome on Windows 10",
      "name" => "[PHP] Pop Up Testing with Chrome on Windows 10",
      "platform" => "Windows 10",
      "browserName" => "Chrome",
      "version" => "85.0"
    );
    return $capabilities;
  }
  
  public function setUp(): void
  {
    $url = "https://". $GLOBALS['LT_USERNAME'] .":" . $GLOBALS['LT_APPKEY'] ."@hub.lambdatest.com/wd/hub";
    $capabilities = $this->build_browser_capabilities();
    /* Скачать Selenium Server 3.141.59 с 
    https://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar
    */
    /* $this->webDriver = RemoteWebDriver::create('http://localhost:4444/wd/hub', $capabilities); */
    $this->webDriver = RemoteWebDriver::create($url, $capabilities);
  }
 
  public function tearDown(): void
  {
    $this->webDriver->quit();
  }
  /*
  * @test
  */ 
  public function test_SwitchToNewWindow()
  {
      $test_url = "http://www.popuptest.com/popuptest1.html";
      $title = "PopupTest 1 - test your popup killer software";
 
      $this->webDriver->get($test_url);
      sleep(5);
 
      /* Это откроет основное окно и шесть всплывающих окон */
      /* После загрузки страницы общее количество страниц будет равным 7 */
      $HandleCount = $this->webDriver->getWindowHandles();
      /* Это ID родительского окна */
      $mainHandle = $HandleCount[0];
      echo ("\n Total number of window handles are " . sizeof($HandleCount));
      echo ("\n Window 0: " . $HandleCount[0]);
 
      $win_title = $this->webDriver->getTitle();
      echo ("\n Title of the parent window is " . $win_title);
 
      foreach( $HandleCount as $handle)
      {
        if($handle != $mainHandle)
        {
          echo ("\n Window handle of the current window: " . $handle);
          $this->webDriver->switchTo()->window($handle);
          echo ("\n Title of the current window: " . $this->webDriver->getTitle());
          /* Закрываем всплывающее окно и возвращаемся к старому окну */
          $this->webDriver->close();
          sleep(2);
        }
      }
      $this->webDriver->switchTo()->window($mainHandle);
      $this->webDriver->manage()->window()->maximize();
      sleep(5);
      $curr_window_title = $this->webDriver->getTitle();
      echo ("\n\n Title of the only left window: " . $curr_window_title);
      $this->assertEquals($curr_window_title, $title);
      sleep(5);
  }
}
?>

❒ Разбор кода


1. Так как для тестирования используется Selenium Grid на LambdaTest, user-name и access key хранятся в глобальных переменных. Того же результата можно получить, перейдя на страницу профиля на LambdaTest.

$GLOBALS['LT_USERNAME'] = "user-name";
# accessKey:  AccessKey можно сгенерировать в панели автоматизации или в разделе профиля
$GLOBALS['LT_APPKEY'] = "access-key";

2. При помощи LambdaTest Capabilities Generator генерируются возможности браузера.

$capabilities = array(
      "build" => "[PHP] Window Switching with Chrome on Windows 10",
      "name" => "[PHP] Window Switching with Chrome on Windows 10",
      "platform" => "Windows 10",
      "browserName" => "Chrome",
      "version" => "85.0"
    );

3. Для доступа к Selenium Grid on LambdaTest (@hub.lambdatest.com/wd/hub) используется сочетание глобальных переменных, в которых хранятся user-name и access-key. Метод create в классе RemoteWebDriver получает в качестве первого параметра URL Selenium Grid, а в качестве второго — возможности браузера.

$url = "https://". $GLOBALS['LT_USERNAME'] .":" . $GLOBALS['LT_APPKEY'] ."@hub.lambdatest.com/wd/hub";
$capabilities = $this->build_browser_capabilities();
$this->webDriver = RemoteWebDriver::create($url, $capabilities);

4. В тестовом методе test_SwitchToNewWindow открывается тестовый URL https://www.lambdatest.com и используется предоставляемый Selenium WebDriver метод getWindowHandle для получения дескриптора текущего окна.

public function test_SwitchToNewWindow()
{
     $test_url_1 = "https://www.lambdatest.com";
     $title_1 = "Most Powerful Cross Browser Testing Tool Online | LambdaTest";
     ...............................................
     ...............................................
     $this->webDriver->get($test_url_1);
     $wHandle = $this->webDriver->getWindowHandle();
     ...............................................
}	

5. Для открытия нового вторичного окна браузера используется метод JavaScript window.open. Методу также передаются ширина и высота окна. Для исполнения сформированного кода JavaScript в контексте текущего открытого окна используется метод executeScript, предоставляемый JavaScriptExecutor в Selenium PHP.

$link = "window.open('". $test_url_2 ."', '_blank', 'toolbar=yes,scrollbars=yes,resizable=yes,width=1200,height=1200')";
$this->webDriver->executeScript($link);

6. Метод getWindowHandles возвращает алфавитно-цифровой массив, содержащий ID (или дескриптор) текущих открытых окон. Оператор sizeof, применяемый к массиву, возвращённому getWindowHandles, возвращает количество открытых окон (т. е. в нашем случае 2).

$HandleCount = $this->webDriver->getWindowHandles();
echo ("\n Total number of window handles are " . sizeof($HandleCount));

7. При помощи утверждения проверяем, не меньше ли двух общее количество дескрипторов окон.

$this->assertEquals(2, sizeof($HandleCount));

8. Для переключения на второе окно (которое было открыто при помощи метода window.open) используется метод switchTo Selenium WebDriver. Если заголовок окна не соответствует ожидаемому заголовку, срабатывает утверждение.

$this->webDriver->switchTo()->window($HandleCount[1]);
$win_title_2 = $this->webDriver->getTitle();
$this->assertEquals($win_title_2, $title_2);

9. Текущее окно закрывается при помощи метода Close().

$this->webDriver->close();

10. Теперь количество дескрипторов окон будет равно одному (так как открыто только родительское окно). Для переключения на родительское окно используется метод switchTo, которому в качестве параметра передаётся дескриптор этого окна (т. е. $wHandle). Если заголовок окна не соответствует ожидаемому заголовку, срабатывает утверждение.

$test_url_1 = "https://www.lambdatest.com";
$title_1 = "Most Powerful Cross Browser Testing Tool Online | LambdaTest";
...........................................
$this->webDriver->get($test_url_1);
...........................................
...........................................
$wHandle = $this->webDriver->getWindowHandle();
 
/* Возвращаемся к окну с дескриптором = 0 */
$this->webDriver->switchTo()->window($wHandle);
/* Проверяем, совпадают ли заголовки окон */
$win_title_1 = $this->webDriver->getTitle();
$this->assertEquals($win_title_1, $title_1);

11. Как часть tearDown вызывается метод quit Selenium WebDriver на PHP.

public function tearDown(): void
{
    $this->webDriver->quit();
}


❒ Исполнение


На скриншоте ниже отмечены дескрипторы всплывающих окон:


После закрытия всех всплывающих окон остаётся только родительское окно, которое закрывается после исполнения теста.


Как показано на снэпшоте исполнения, сделанном на вкладке автоматизации платформы LambdaTest, всплывающие окна открываются, а затем закрываются в обратном хронологическом порядке.



Заключение


Окна браузера, в том числе вкладки и всплывающие окна, идентифицируются по дескрипторам окон. Эти дескрипторы используются в качестве ID окон, они уникальны для каждого окна браузера. В этом туториале по Selenium WebDriver PHP мы узнали, как происходит работа с окнами в Selenium с PHP при помощи таких методов, как switchTo, getWindowHandle и getWindowHandles. Эти методы играют важную роль в работе с окнами для автоматизации тестов Selenium с PHP.

Tags:
Hubs:
Total votes 29: ↑29 and ↓0+29
Comments0

Articles

Information

Website
ruvds.com
Registered
Founded
Employees
11–30 employees
Location
Россия
Representative
ruvds