Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / modules / language / tests / src / Functional / LanguageSwitchingTest.php
1 <?php
2
3 namespace Drupal\Tests\language\Functional;
4
5 use Drupal\language\Entity\ConfigurableLanguage;
6 use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
7 use Drupal\menu_link_content\Entity\MenuLinkContent;
8 use Drupal\Core\Language\LanguageInterface;
9 use Drupal\Tests\BrowserTestBase;
10
11 /**
12  * Functional tests for the language switching feature.
13  *
14  * @group language
15  */
16 class LanguageSwitchingTest extends BrowserTestBase {
17
18   /**
19    * Modules to enable.
20    *
21    * @var array
22    */
23   public static $modules = ['locale', 'locale_test', 'language', 'block', 'language_test', 'menu_ui'];
24
25   protected function setUp() {
26     parent::setUp();
27
28     // Create and log in user.
29     $admin_user = $this->drupalCreateUser(['administer blocks', 'administer languages', 'access administration pages']);
30     $this->drupalLogin($admin_user);
31   }
32
33   /**
34    * Functional tests for the language switcher block.
35    */
36   public function testLanguageBlock() {
37     // Add language.
38     $edit = [
39       'predefined_langcode' => 'fr',
40     ];
41     $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
42
43     // Set the native language name.
44     $this->saveNativeLanguageName('fr', 'français');
45
46     // Enable URL language detection and selection.
47     $edit = ['language_interface[enabled][language-url]' => '1'];
48     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
49
50     // Enable the language switching block.
51     $block = $this->drupalPlaceBlock('language_block:' . LanguageInterface::TYPE_INTERFACE, [
52       'id' => 'test_language_block',
53       // Ensure a 2-byte UTF-8 sequence is in the tested output.
54       'label' => $this->randomMachineName(8) . '×',
55     ]);
56
57     $this->doTestLanguageBlockAuthenticated($block->label());
58     $this->doTestLanguageBlockAnonymous($block->label());
59   }
60
61   /**
62    * For authenticated users, the "active" class is set by JavaScript.
63    *
64    * @param string $block_label
65    *   The label of the language switching block.
66    *
67    * @see testLanguageBlock()
68    */
69   protected function doTestLanguageBlockAuthenticated($block_label) {
70     // Assert that the language switching block is displayed on the frontpage.
71     $this->drupalGet('');
72     $this->assertText($block_label, 'Language switcher block found.');
73
74     // Assert that each list item and anchor element has the appropriate data-
75     // attributes.
76     $language_switchers = $this->xpath('//div[@id=:id]/ul/li', [':id' => 'block-test-language-block']);
77     $list_items = [];
78     $anchors = [];
79     $labels = [];
80     foreach ($language_switchers as $list_item) {
81       $classes = explode(" ", $list_item->getAttribute('class'));
82       list($langcode) = array_intersect($classes, ['en', 'fr']);
83       $list_items[] = [
84         'langcode_class' => $langcode,
85         'data-drupal-link-system-path' => $list_item->getAttribute('data-drupal-link-system-path'),
86       ];
87
88       $link = $list_item->find('xpath', 'a');
89       $anchors[] = [
90          'hreflang' => $link->getAttribute('hreflang'),
91          'data-drupal-link-system-path' => $link->getAttribute('data-drupal-link-system-path'),
92       ];
93       $labels[] = $link->getText();
94     }
95     $expected_list_items = [
96       0 => ['langcode_class' => 'en', 'data-drupal-link-system-path' => 'user/2'],
97       1 => ['langcode_class' => 'fr', 'data-drupal-link-system-path' => 'user/2'],
98     ];
99     $this->assertIdentical($list_items, $expected_list_items, 'The list items have the correct attributes that will allow the drupal.active-link library to mark them as active.');
100     $expected_anchors = [
101       0 => ['hreflang' => 'en', 'data-drupal-link-system-path' => 'user/2'],
102       1 => ['hreflang' => 'fr', 'data-drupal-link-system-path' => 'user/2'],
103     ];
104     $this->assertIdentical($anchors, $expected_anchors, 'The anchors have the correct attributes that will allow the drupal.active-link library to mark them as active.');
105     $settings = $this->getDrupalSettings();
106     $this->assertIdentical($settings['path']['currentPath'], 'user/2', 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.');
107     $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.');
108     $this->assertIdentical($settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.');
109     $this->assertIdentical($labels, ['English', 'français'], 'The language links labels are in their own language on the language switcher block.');
110   }
111
112   /**
113    * For anonymous users, the "active" class is set by PHP.
114    *
115    * @param string $block_label
116    *   The label of the language switching block.
117    *
118    * @see testLanguageBlock()
119    */
120   protected function doTestLanguageBlockAnonymous($block_label) {
121     $this->drupalLogout();
122
123     // Assert that the language switching block is displayed on the frontpage
124     // and ensure that the active class is added when query params are present.
125     $this->drupalGet('', ['query' => ['foo' => 'bar']]);
126     $this->assertText($block_label, 'Language switcher block found.');
127
128     // Assert that only the current language is marked as active.
129     $language_switchers = $this->xpath('//div[@id=:id]/ul/li', [':id' => 'block-test-language-block']);
130     $links = [
131       'active' => [],
132       'inactive' => [],
133     ];
134     $anchors = [
135       'active' => [],
136       'inactive' => [],
137     ];
138     $labels = [];
139     foreach ($language_switchers as $list_item) {
140       $classes = explode(" ", $list_item->getAttribute('class'));
141       list($langcode) = array_intersect($classes, ['en', 'fr']);
142       if (in_array('is-active', $classes)) {
143         $links['active'][] = $langcode;
144       }
145       else {
146         $links['inactive'][] = $langcode;
147       }
148
149       $link = $list_item->find('xpath', 'a');
150       $anchor_classes = explode(" ", $link->getAttribute('class'));
151       if (in_array('is-active', $anchor_classes)) {
152         $anchors['active'][] = $langcode;
153       }
154       else {
155         $anchors['inactive'][] = $langcode;
156       }
157       $labels[] = $link->getText();
158     }
159     $this->assertIdentical($links, ['active' => ['en'], 'inactive' => ['fr']], 'Only the current language list item is marked as active on the language switcher block.');
160     $this->assertIdentical($anchors, ['active' => ['en'], 'inactive' => ['fr']], 'Only the current language anchor is marked as active on the language switcher block.');
161     $this->assertIdentical($labels, ['English', 'français'], 'The language links labels are in their own language on the language switcher block.');
162   }
163
164   /**
165    * Test language switcher links for domain based negotiation.
166    */
167   public function testLanguageBlockWithDomain() {
168     // Add the Italian language.
169     ConfigurableLanguage::createFromLangcode('it')->save();
170
171     // Rebuild the container so that the new language is picked up by services
172     // that hold a list of languages.
173     $this->rebuildContainer();
174
175     $languages = $this->container->get('language_manager')->getLanguages();
176
177     // Enable browser and URL language detection.
178     $edit = [
179       'language_interface[enabled][language-url]' => TRUE,
180       'language_interface[weight][language-url]' => -10,
181     ];
182     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
183
184     // Do not allow blank domain.
185     $edit = [
186       'language_negotiation_url_part' => LanguageNegotiationUrl::CONFIG_DOMAIN,
187       'domain[en]' => '',
188     ];
189     $this->drupalPostForm('admin/config/regional/language/detection/url', $edit, t('Save configuration'));
190     $this->assertText(t('The domain may not be left blank for English'), 'The form does not allow blank domains.');
191
192     // Change the domain for the Italian language.
193     $edit = [
194       'language_negotiation_url_part' => LanguageNegotiationUrl::CONFIG_DOMAIN,
195       'domain[en]' => \Drupal::request()->getHost(),
196       'domain[it]' => 'it.example.com',
197     ];
198     $this->drupalPostForm('admin/config/regional/language/detection/url', $edit, t('Save configuration'));
199     $this->assertText(t('The configuration options have been saved'), 'Domain configuration is saved.');
200
201     // Enable the language switcher block.
202     $this->drupalPlaceBlock('language_block:' . LanguageInterface::TYPE_INTERFACE, ['id' => 'test_language_block']);
203
204     $this->drupalGet('');
205
206     /** @var \Drupal\Core\Routing\UrlGenerator $generator */
207     $generator = $this->container->get('url_generator');
208
209     // Verify the English URL is correct
210     list($english_link) = $this->xpath('//div[@id=:id]/ul/li/a[@hreflang=:hreflang]', [
211       ':id' => 'block-test-language-block',
212       ':hreflang' => 'en',
213     ]);
214     $english_url = $generator->generateFromRoute('entity.user.canonical', ['user' => 2], ['language' => $languages['en']]);
215     $this->assertEqual($english_url, $english_link->getAttribute('href'));
216
217     // Verify the Italian URL is correct
218     list($italian_link) = $this->xpath('//div[@id=:id]/ul/li/a[@hreflang=:hreflang]', [
219       ':id' => 'block-test-language-block',
220       ':hreflang' => 'it',
221     ]);
222     $italian_url = $generator->generateFromRoute('entity.user.canonical', ['user' => 2], ['language' => $languages['it']]);
223     $this->assertEqual($italian_url, $italian_link->getAttribute('href'));
224   }
225
226   /**
227    * Test active class on links when switching languages.
228    */
229   public function testLanguageLinkActiveClass() {
230     // Add language.
231     $edit = [
232       'predefined_langcode' => 'fr',
233     ];
234     $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
235
236     // Enable URL language detection and selection.
237     $edit = ['language_interface[enabled][language-url]' => '1'];
238     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
239
240     $this->doTestLanguageLinkActiveClassAuthenticated();
241     $this->doTestLanguageLinkActiveClassAnonymous();
242   }
243
244   /**
245    * Check the path-admin class, as same as on default language.
246    */
247   public function testLanguageBodyClass() {
248     $searched_class = 'path-admin';
249
250     // Add language.
251     $edit = [
252       'predefined_langcode' => 'fr',
253     ];
254     $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
255
256     // Enable URL language detection and selection.
257     $edit = ['language_interface[enabled][language-url]' => '1'];
258     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
259
260     // Check if the default (English) admin/config page has the right class.
261     $this->drupalGet('admin/config');
262     $class = $this->xpath('//body[contains(@class, :class)]', [':class' => $searched_class]);
263     $this->assertTrue(isset($class[0]), t('The path-admin class appears on default language.'));
264
265     // Check if the French admin/config page has the right class.
266     $this->drupalGet('fr/admin/config');
267     $class = $this->xpath('//body[contains(@class, :class)]', [':class' => $searched_class]);
268     $this->assertTrue(isset($class[0]), t('The path-admin class same as on default language.'));
269
270     // The testing profile sets the user/login page as the frontpage. That
271     // redirects authenticated users to their profile page, so check with an
272     // anonymous user instead.
273     $this->drupalLogout();
274
275     // Check if the default (English) frontpage has the right class.
276     $this->drupalGet('<front>');
277     $class = $this->xpath('//body[contains(@class, :class)]', [':class' => 'path-frontpage']);
278     $this->assertTrue(isset($class[0]), 'path-frontpage class found on the body tag');
279
280     // Check if the French frontpage has the right class.
281     $this->drupalGet('fr');
282     $class = $this->xpath('//body[contains(@class, :class)]', [':class' => 'path-frontpage']);
283     $this->assertTrue(isset($class[0]), 'path-frontpage class found on the body tag with french as the active language');
284
285   }
286
287   /**
288    * For authenticated users, the "active" class is set by JavaScript.
289    *
290    * @see testLanguageLinkActiveClass()
291    */
292   protected function doTestLanguageLinkActiveClassAuthenticated() {
293     $function_name = '#type link';
294     $path = 'language_test/type-link-active-class';
295
296     // Test links generated by the link generator on an English page.
297     $current_language = 'English';
298     $this->drupalGet($path);
299
300     // Language code 'none' link should be active.
301     $langcode = 'none';
302     $links = $this->xpath('//a[@id = :id and @data-drupal-link-system-path = :path]', [':id' => 'no_lang_link', ':path' => $path]);
303     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
304
305     // Language code 'en' link should be active.
306     $langcode = 'en';
307     $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', [':id' => 'en_link', ':lang' => 'en', ':path' => $path]);
308     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
309
310     // Language code 'fr' link should not be active.
311     $langcode = 'fr';
312     $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', [':id' => 'fr_link', ':lang' => 'fr', ':path' => $path]);
313     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to NOT mark it as active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
314
315     // Verify that drupalSettings contains the correct values.
316     $settings = $this->getDrupalSettings();
317     $this->assertIdentical($settings['path']['currentPath'], $path, 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.');
318     $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.');
319     $this->assertIdentical($settings['path']['currentLanguage'], 'en', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.');
320
321     // Test links generated by the link generator on a French page.
322     $current_language = 'French';
323     $this->drupalGet('fr/language_test/type-link-active-class');
324
325     // Language code 'none' link should be active.
326     $langcode = 'none';
327     $links = $this->xpath('//a[@id = :id and @data-drupal-link-system-path = :path]', [':id' => 'no_lang_link', ':path' => $path]);
328     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
329
330     // Language code 'en' link should not be active.
331     $langcode = 'en';
332     $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', [':id' => 'en_link', ':lang' => 'en', ':path' => $path]);
333     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to NOT mark it as active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
334
335     // Language code 'fr' link should be active.
336     $langcode = 'fr';
337     $links = $this->xpath('//a[@id = :id and @hreflang = :lang and @data-drupal-link-system-path = :path]', [':id' => 'fr_link', ':lang' => 'fr', ':path' => $path]);
338     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode has the correct attributes that will allow the drupal.active-link library to mark it as active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
339
340     // Verify that drupalSettings contains the correct values.
341     $settings = $this->getDrupalSettings();
342     $this->assertIdentical($settings['path']['currentPath'], $path, 'drupalSettings.path.currentPath is set correctly to allow drupal.active-link to mark the correct links as active.');
343     $this->assertIdentical($settings['path']['isFront'], FALSE, 'drupalSettings.path.isFront is set correctly to allow drupal.active-link to mark the correct links as active.');
344     $this->assertIdentical($settings['path']['currentLanguage'], 'fr', 'drupalSettings.path.currentLanguage is set correctly to allow drupal.active-link to mark the correct links as active.');
345   }
346
347   /**
348    * For anonymous users, the "active" class is set by PHP.
349    *
350    * @see testLanguageLinkActiveClass()
351    */
352   protected function doTestLanguageLinkActiveClassAnonymous() {
353     $function_name = '#type link';
354
355     $this->drupalLogout();
356
357     // Test links generated by the link generator on an English page.
358     $current_language = 'English';
359     $this->drupalGet('language_test/type-link-active-class');
360
361     // Language code 'none' link should be active.
362     $langcode = 'none';
363     $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', [':id' => 'no_lang_link', ':class' => 'is-active']);
364     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
365
366     // Language code 'en' link should be active.
367     $langcode = 'en';
368     $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', [':id' => 'en_link', ':class' => 'is-active']);
369     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
370
371     // Language code 'fr' link should not be active.
372     $langcode = 'fr';
373     $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', [':id' => 'fr_link', ':class' => 'is-active']);
374     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
375
376     // Test links generated by the link generator on a French page.
377     $current_language = 'French';
378     $this->drupalGet('fr/language_test/type-link-active-class');
379
380     // Language code 'none' link should be active.
381     $langcode = 'none';
382     $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', [':id' => 'no_lang_link', ':class' => 'is-active']);
383     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
384
385     // Language code 'en' link should not be active.
386     $langcode = 'en';
387     $links = $this->xpath('//a[@id = :id and not(contains(@class, :class))]', [':id' => 'en_link', ':class' => 'is-active']);
388     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is NOT marked active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
389
390     // Language code 'fr' link should be active.
391     $langcode = 'fr';
392     $links = $this->xpath('//a[@id = :id and contains(@class, :class)]', [':id' => 'fr_link', ':class' => 'is-active']);
393     $this->assertTrue(isset($links[0]), t('A link generated by :function to the current :language page with langcode :langcode is marked active.', [':function' => $function_name, ':language' => $current_language, ':langcode' => $langcode]));
394   }
395
396   /**
397    * Tests language switcher links for session based negotiation.
398    */
399   public function testLanguageSessionSwitchLinks() {
400     // Add language.
401     $edit = [
402       'predefined_langcode' => 'fr',
403     ];
404     $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
405
406     // Enable session language detection and selection.
407     $edit = [
408       'language_interface[enabled][language-url]' => FALSE,
409       'language_interface[enabled][language-session]' => TRUE,
410     ];
411     $this->drupalPostForm('admin/config/regional/language/detection', $edit, t('Save settings'));
412
413     // Enable the language switching block.
414     $this->drupalPlaceBlock('language_block:' . LanguageInterface::TYPE_INTERFACE, [
415       'id' => 'test_language_block',
416     ]);
417
418     // Enable the main menu block.
419     $this->drupalPlaceBlock('system_menu_block:main', [
420       'id' => 'test_menu',
421     ]);
422
423     // Add a link to the homepage.
424     $link = MenuLinkContent::create([
425       'title' => 'Home',
426       'menu_name' => 'main',
427       'bundle' => 'menu_link_content',
428       'link' => [['uri' => 'entity:user/2']],
429     ]);
430     $link->save();
431
432     // Go to the homepage.
433     $this->drupalGet('');
434     // Click on the French link.
435     $this->clickLink(t('French'));
436     // There should be a query parameter to set the session language.
437     $this->assertUrl('user/2', ['query' => ['language' => 'fr']]);
438     // Click on the 'Home' Link.
439     $this->clickLink(t('Home'));
440     // There should be no query parameter.
441     $this->assertUrl('user/2');
442     // Click on the French link.
443     $this->clickLink(t('French'));
444     // There should be no query parameter.
445     $this->assertUrl('user/2');
446   }
447
448   /**
449    * Saves the native name of a language entity in configuration as a label.
450    *
451    * @param string $langcode
452    *   The language code of the language.
453    * @param string $label
454    *   The native name of the language.
455    */
456   protected function saveNativeLanguageName($langcode, $label) {
457     \Drupal::service('language.config_factory_override')
458       ->getOverride($langcode, 'language.entity.' . $langcode)->set('label', $label)->save();
459   }
460
461 }