1 /*
2     This file is part of the KDE libraries
3     SPDX-FileCopyrightText: 2007-2009 David Faure <faure@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "kxmlgui_unittest.h"
9 #include "testguiclient.h"
10 #include "testxmlguiwindow.h"
11 
12 #include <QAction>
13 #include <QDebug>
14 #include <QDialogButtonBox>
15 #include <QDir>
16 #include <QMenuBar>
17 #include <QPushButton>
18 #include <QShowEvent>
19 #include <QTest>
20 
21 #include <KConfigGroup>
22 #include <KSharedConfig>
23 
24 #include <kedittoolbar.h>
25 #include <kswitchlanguagedialog_p.h>
26 #include <kxmlguibuilder.h>
27 #include <kxmlguiclient.h>
28 #include <kxmlguiversionhandler.cpp> // it's not exported, so we need to include the code here
29 
30 QTEST_MAIN(KXmlGui_UnitTest)
31 
32 enum Flags {
33     NoFlags = 0,
34     AddToolBars = 1,
35     AddModifiedToolBars = 2,
36     AddActionProperties = 4,
37     AddModifiedMenus = 8,
38     // next item is 16
39 };
40 
createXmlFile(QFile & file,int version,int flags,const QByteArray & toplevelTag="gui")41 static void createXmlFile(QFile &file, int version, int flags, const QByteArray &toplevelTag = "gui")
42 {
43     const QByteArray xml =
44         "<?xml version = '1.0'?>\n"
45         "<!DOCTYPE " + toplevelTag + " SYSTEM \"kpartgui.dtd\">\n"
46         "<" + toplevelTag + " version=\"" + QByteArray::number(version) + "\" name=\"foo\" >\n"
47         "<MenuBar>\n";
48     file.write(xml);
49     if (flags & AddModifiedMenus) {
50         file.write(
51             "<Menu noMerge=\"1\" name=\"file\" >\n"
52             "<text>&amp;File</text>\n"
53             "<Action name=\"file_open\" />\n"
54             "</Menu>\n");
55     }
56     file.write("</MenuBar>\n");
57     if (flags & AddToolBars) {
58         file.write(
59             "<ToolBar name=\"mainToolBar\">\n"
60             "  <text>Main Toolbar</text>\n"
61             "  <Action name=\"print\" />\n"
62             "</ToolBar>\n"
63             "<ToolBar name=\"bookmarkToolBar\">\n"
64             "  <text>Bookmark Toolbar</text>\n"
65             "</ToolBar>\n"
66             "<ToolBar name=\"newToolBar\">\n"
67             "  <text>New Toolbar</text>\n"
68             "</ToolBar>\n");
69     }
70     if (flags & AddModifiedToolBars) {
71         file.write(
72             "<ToolBar name=\"mainToolBar\">\n"
73             "  <text>Main Toolbar</text>\n"
74             "  <Action name=\"home\" />\n"
75             "</ToolBar>\n"
76             "<ToolBar name=\"bookmarkToolBar\">\n"
77             "  <text>Modified toolbars</text>\n"
78             "</ToolBar>\n");
79     }
80     if (flags & AddActionProperties) {
81         file.write(
82             "<ActionProperties>\n"
83             "  <Action shortcut=\"F9\" name=\"konq_sidebartng\" />\n"
84             "</ActionProperties>\n");
85     }
86     file.write("</" + toplevelTag + ">\n");
87 }
88 
clickApply(KEditToolBar * dialog)89 static void clickApply(KEditToolBar *dialog)
90 {
91     QDialogButtonBox *box = dialog->findChild<QDialogButtonBox *>();
92     Q_ASSERT(box != nullptr);
93     box->button(QDialogButtonBox::Apply)->setEnabled(true);
94     box->button(QDialogButtonBox::Apply)->click();
95 }
96 
initTestCase()97 void KXmlGui_UnitTest::initTestCase()
98 {
99     QStandardPaths::setTestModeEnabled(true);
100     // Leftover configuration breaks testAutoSaveSettings
101     const QString configFile = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, KSharedConfig::openConfig()->name());
102     if (!configFile.isEmpty()) {
103         qDebug() << "Removing old config file";
104         QFile::remove(configFile);
105         KSharedConfig::openConfig()->reparseConfiguration();
106     }
107 }
108 
testFindVersionNumber_data()109 void KXmlGui_UnitTest::testFindVersionNumber_data()
110 {
111     QTest::addColumn<QString>("xml");
112     QTest::addColumn<QString>("version");
113 
114     QTest::newRow("simple test") << "<?xml version = '1.0'?>\n"
115                                     "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
116                                     "<gui version=\"3\" name=\"foo\"/>\n"
117                                  << "3";
118     QTest::newRow("two digits") << "<?xml version = '1.0'?>\n"
119                                    "<gui version=\"42\" name=\"foo\"/>\n"
120                                 << "42";
121     QTest::newRow("with spaces") << // as found in dirfilterplugin.rc for instance
122         "<?xml version = '1.0'?>\n"
123         "<gui version = \"1\" name=\"foo\"/>\n"
124                                  << "1";
125     QTest::newRow("with a dot") << // as was found in autorefresh.rc
126         "<?xml version = '1.0'?>\n"
127         "<gui version = \"0.2\" name=\"foo\"/>\n"
128                                 << QString() /*error*/;
129     QTest::newRow("with a comment") << // as found in kmail.rc
130         "<!DOCTYPE gui>\n"
131         "<!-- This file should be synchronized with kmail_part.rc to provide\n"
132         "the same menu entries at the same place in KMail and Kontact  -->\n"
133         "<gui version=\"452\" name=\"kmmainwin\">\n"
134                                     << "452";
135 }
136 
testFindVersionNumber()137 void KXmlGui_UnitTest::testFindVersionNumber()
138 {
139     QFETCH(QString, xml);
140     QFETCH(QString, version);
141     QCOMPARE(KXMLGUIClient::findVersionNumber(xml), version);
142 }
143 
testVersionHandlerSameVersion()144 void KXmlGui_UnitTest::testVersionHandlerSameVersion()
145 {
146     // This emulates the case where the user has modified stuff locally
147     // and the application hasn't changed since, so the version number is unchanged.
148     QTemporaryFile userFile;
149     QVERIFY(userFile.open());
150     createXmlFile(userFile, 2, AddActionProperties | AddModifiedToolBars);
151     const QString firstFile = userFile.fileName();
152 
153     QTemporaryFile appFile;
154     QVERIFY(appFile.open());
155     createXmlFile(appFile, 2, AddToolBars);
156     const QString secondFile = appFile.fileName();
157 
158     QStringList files;
159     files << firstFile << secondFile;
160 
161     userFile.close();
162     appFile.close();
163 
164     KXmlGuiVersionHandler versionHandler(files);
165     QCOMPARE(versionHandler.finalFile(), firstFile);
166     QString finalDoc = versionHandler.finalDocument();
167     QVERIFY(finalDoc.startsWith(QLatin1String("<?xml")));
168     // Check that the shortcuts defined by the user were kept
169     QVERIFY(finalDoc.contains(QLatin1String("<ActionProperties>")));
170     QVERIFY(finalDoc.contains(QLatin1String("sidebartng")));
171     // Check that the toolbars modified by the user were kept
172     QVERIFY(finalDoc.contains(QLatin1String("<Action name=\"home\"")));
173     // and that the toolbar that isn't in the local file, isn't there in the GUI
174     QVERIFY(!finalDoc.contains(QLatin1String("<ToolBar name=\"newToolBar\"")));
175 
176     QVERIFY(userFile.open());
177     const QString userFileContents = QString::fromUtf8(userFile.readAll());
178     QCOMPARE(finalDoc, userFileContents);
179 }
180 
testVersionHandlerNewVersionNothingKept()181 void KXmlGui_UnitTest::testVersionHandlerNewVersionNothingKept()
182 {
183     // This emulates the case where the application has been upgraded
184     // and the user has a local ui.rc file, but without shortcuts or toolbar changes.
185     // Not sure how this can happen - would be a menu-only app and hand editing !?
186     // Anyway the point is to test version number comparison :)
187 
188     QMap<QString, int> fileToVersionMap; // makes QCOMPARE failures more readable than just temp filenames
189 
190     const QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kxmlgui_unittest");
191     QDir().mkpath(dir);
192     QFile fileV2(dir + QStringLiteral("/testui.rc"));
193     QVERIFY2(fileV2.open(QIODevice::WriteOnly), qPrintable(fileV2.fileName()));
194     createXmlFile(fileV2, 2, NoFlags);
195     fileToVersionMap.insert(fileV2.fileName(), 2);
196 
197     QTemporaryFile fileV5;
198     QVERIFY(fileV5.open());
199     createXmlFile(fileV5, 5, NoFlags);
200     fileToVersionMap.insert(fileV5.fileName(), 5);
201 
202     // The highest version is neither the first nor last one in the list,
203     // to make sure the code really selects the highest version, not just by chance :)
204     // (This is why we add the v1 version at the end of the list)
205     QTemporaryFile fileV1;
206     QVERIFY(fileV1.open());
207     createXmlFile(fileV1, 1, NoFlags);
208     fileToVersionMap.insert(fileV1.fileName(), 1);
209 
210     QStringList files;
211     files << fileV2.fileName() << fileV5.fileName() << fileV1.fileName();
212 
213     fileV2.close();
214     fileV5.close();
215     fileV1.close();
216 
217     KXmlGuiVersionHandler versionHandler(files);
218     QCOMPARE(fileToVersionMap.value(versionHandler.finalFile()), 5);
219     QString finalDoc = versionHandler.finalDocument();
220     QVERIFY(finalDoc.startsWith(QLatin1String("<?xml")));
221     QVERIFY(finalDoc.contains(QLatin1String("version=\"5\"")));
222 
223     QVERIFY(fileV5.open());
224     const QString fileV5Contents = QString::fromUtf8(fileV5.readAll());
225     QCOMPARE(finalDoc, fileV5Contents);
226 }
227 
testVersionHandlerNewVersionUserChanges()228 void KXmlGui_UnitTest::testVersionHandlerNewVersionUserChanges()
229 {
230     // This emulates the case where the application has been upgraded
231     // after the user has changed shortcuts and toolbars
232 
233     QMap<QString, int> fileToVersionMap; // makes QCOMPARE failures more readable than just temp filenames
234 
235     // local file
236     const QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kxmlgui_unittest");
237     QFile fileV2(dir + QLatin1String("/testui.rc"));
238     QVERIFY(fileV2.open(QIODevice::WriteOnly));
239     createXmlFile(fileV2, 2, AddActionProperties | AddModifiedToolBars);
240     fileToVersionMap.insert(fileV2.fileName(), 2);
241 
242     // more-global (application) file
243     QTemporaryFile fileV5;
244     QVERIFY(fileV5.open());
245     createXmlFile(fileV5, 5, AddToolBars | AddModifiedMenus, "kpartgui");
246     fileToVersionMap.insert(fileV5.fileName(), 5);
247 
248     // The highest version is neither the first nor last one in the list,
249     // to make sure the code really selects the highest version, not just by chance :)
250     // (This is why we add the v1 version at the end of the list)
251     QTemporaryFile fileV1;
252     QVERIFY(fileV1.open());
253     createXmlFile(fileV1, 1, AddToolBars);
254     fileToVersionMap.insert(fileV1.fileName(), 1);
255 
256     QStringList files;
257     files << fileV2.fileName() << fileV5.fileName() << fileV1.fileName();
258 
259     fileV2.close();
260     fileV5.close();
261     fileV1.close();
262 
263     KXmlGuiVersionHandler versionHandler(files);
264     // We end up with the local file, so in our map it has version 2.
265     // But of course by now it says "version=5" in it :)
266     QCOMPARE(fileToVersionMap.value(versionHandler.finalFile()), 2);
267     const QString finalDoc = versionHandler.finalDocument();
268     // qDebug() << finalDoc;
269     QVERIFY(finalDoc.startsWith(QLatin1String("<?xml")));
270     QVERIFY(finalDoc.contains(QLatin1String("version=\"5\"")));
271     // Check that the shortcuts defined by the user were kept
272     QVERIFY(finalDoc.contains(QLatin1String("<ActionProperties>")));
273     QVERIFY(finalDoc.contains(QLatin1String("sidebartng")));
274     // Check that the menus modified by the app are still there
275     QVERIFY(finalDoc.contains(QLatin1String("<Action name=\"file_open\"")));
276     // Check that the toolbars modified by the user were kept
277     QVERIFY(finalDoc.contains(QLatin1String("<Action name=\"home\"")));
278     // Check that the toolbars added by the application were kept (https://invent.kde.org/graphics/okular/-/merge_requests/197)
279     QVERIFY(finalDoc.contains(QLatin1String("<ToolBar name=\"newToolBar\"")));
280 }
281 
collectMenuNames(KXMLGUIFactory & factory)282 static QStringList collectMenuNames(KXMLGUIFactory &factory)
283 {
284     const QList<QWidget *> containers = factory.containers(QStringLiteral("Menu"));
285     QStringList containerNames;
286     for (QWidget *w : containers) {
287         containerNames << w->objectName();
288     }
289     return containerNames;
290 }
291 
debugActions(const QList<QAction * > & actions)292 void debugActions(const QList<QAction *> &actions)
293 {
294     for (QAction *action : actions) {
295         qDebug() << (action->isSeparator() ? QString::fromLatin1("separator") : action->objectName());
296     }
297 }
298 
checkActions(const QList<QAction * > & actions,const QStringList & expectedActions)299 static void checkActions(const QList<QAction *> &actions, const QStringList &expectedActions)
300 {
301     for (int i = 0; i < expectedActions.count(); ++i) {
302         if (i >= actions.count()) {
303             break;
304         }
305         QAction *action = actions.at(i);
306         if (action->isSeparator()) {
307             QCOMPARE(QStringLiteral("separator"), expectedActions[i]);
308         } else {
309             QCOMPARE(action->objectName(), expectedActions[i]);
310         }
311     }
312     QCOMPARE(actions.count(), expectedActions.count());
313 }
314 
testPartMerging()315 void KXmlGui_UnitTest::testPartMerging()
316 {
317     const QByteArray hostXml =
318         "<?xml version = '1.0'?>\n"
319         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
320         "<gui version=\"1\" name=\"foo\" >\n"
321         "<MenuBar>\n"
322         " <Menu name=\"go\"><text>&amp;Go</text>\n"
323         "  <!-- go_up, go_back, go_forward, go_home, separator: coming from ui_standards.rc -->\n"
324         "  <DefineGroup name=\"before_merge\"/>\n"
325         "  <Merge/>\n"
326         "  <Action name=\"host_after_merge\"/>\n"
327         "  <Action name=\"host_after_merge_2\"/>\n"
328         "  <Separator/>\n"
329         "  <DefineGroup name=\"new_merge\"/>\n"
330         "  <Title>Section title</Title>\n"
331         "  <Action name=\"last_from_host\"/>\n"
332         " </Menu>\n"
333         " <Menu name=\"file\"><text>&amp;File</text>\n"
334         "  <DefineGroup name=\"placed_merge\" append=\"new_merge\"/>\n"
335         " </Menu>\n"
336         "</MenuBar>\n"
337         "</gui>\n";
338 
339     TestGuiClient hostClient;
340     hostClient.createActions(QStringList() << QStringLiteral("go_up") << QStringLiteral("go_back") << QStringLiteral("go_forward") << QStringLiteral("go_home")
341                                            << QStringLiteral("host_after_merge") << QStringLiteral("host_after_merge_2") << QStringLiteral("last_from_host")
342                                            << QStringLiteral("file_new") << QStringLiteral("file_open") << QStringLiteral("file_quit"));
343     hostClient.createGUI(hostXml, true /*ui_standards.rc*/);
344     KMainWindow mainWindow;
345     KXMLGUIBuilder builder(&mainWindow);
346     KXMLGUIFactory factory(&builder);
347     factory.addClient(&hostClient);
348 
349     const QString hostDomDoc = hostClient.domDocument().toString();
350 
351     QWidget *goMenuW = factory.container(QStringLiteral("go"), &hostClient);
352     QVERIFY(goMenuW);
353     QMenu *goMenu = qobject_cast<QMenu *>(goMenuW);
354     QVERIFY(goMenu);
355     QMenu *fileMenu = qobject_cast<QMenu *>(factory.container(QStringLiteral("file"), &hostClient));
356 
357     // debugActions(goMenu->actions());
358     // clang-format off
359     checkActions(goMenu->actions(), QStringList()
360                  << QStringLiteral("go_up")
361                  << QStringLiteral("go_back")
362                  << QStringLiteral("go_forward")
363                  << QStringLiteral("go_home")
364                  << QStringLiteral("separator")
365                  << QStringLiteral("host_after_merge")
366                  << QStringLiteral("host_after_merge_2")
367                  << QStringLiteral("separator")
368                  << QStringLiteral("separator") // <Title> separator
369                  << QStringLiteral("last_from_host"));
370     checkActions(fileMenu->actions(), QStringList()
371                  << QStringLiteral("file_new")
372                  << QStringLiteral("file_open")
373                  << QStringLiteral("separator")
374                  << QStringLiteral("file_quit"));
375     // clang-format on
376 
377     qDebug() << "Now merging the part";
378 
379     const QByteArray partXml =
380         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
381         "<gui version=\"1\" name=\"part\" >\n"
382         "<MenuBar>\n"
383         " <Menu name=\"go\"><text>&amp;Go</text>\n"
384         "  <Action name=\"go_previous\" group=\"before_merge\"/>\n"
385         "  <Action name=\"go_next\" group=\"before_merge\"/>\n"
386         "  <Separator/>\n"
387         "  <Action name=\"first_page\"/>\n"
388         "  <Action name=\"last_page\"/>\n"
389         "  <Title>Part Section</Title>\n"
390         "  <ActionList name=\"action_list\"/>\n"
391         "  <Action name=\"action_in_merge_group\" group=\"new_merge\"/>\n"
392         "  <Action name=\"undefined_group\" group=\"no_such_merge\"/>\n"
393         "  <Action name=\"last_from_part\"/>\n"
394         " </Menu>\n"
395         " <Menu name=\"file\"><text>&amp;File</text>\n"
396         "  <Action group=\"placed_merge\" name=\"action_in_placed_merge\"/>\n"
397         "  <Action name=\"other_file_action\"/>\n"
398         " </Menu>\n"
399         "</MenuBar>\n"
400         "</gui>\n";
401 
402     TestGuiClient partClient(partXml);
403     partClient.createActions(QStringList() << QStringLiteral("go_previous") << QStringLiteral("go_next") << QStringLiteral("first_page")
404                                            << QStringLiteral("last_page") << QStringLiteral("last_from_part") << QStringLiteral("action_in_merge_group")
405                                            << QStringLiteral("undefined_group") << QStringLiteral("action_in_placed_merge")
406                                            << QStringLiteral("other_file_action") << QStringLiteral("action1") << QStringLiteral("action2"));
407     const QList<QAction *> actionList = {partClient.actionCollection()->action(QStringLiteral("action1")),
408                                          partClient.actionCollection()->action(QStringLiteral("action2"))};
409 
410     for (int i = 0; i < 5; ++i) {
411         // qDebug() << "addClient, iteration" << i;
412         factory.addClient(&partClient);
413         partClient.plugActionList(QStringLiteral("action_list"), actionList);
414 
415         // clang-format off
416         //debugActions(goMenu->actions());
417         checkActions(goMenu->actions(), QStringList()
418                 << QStringLiteral("go_up")
419                 << QStringLiteral("go_back")
420                 << QStringLiteral("go_forward")
421                 << QStringLiteral("go_home")
422                 << QStringLiteral("separator")
423                 << QStringLiteral("go_previous")
424                 << QStringLiteral("go_next")
425                 // Contents of the <Merge>:
426                 << QStringLiteral("separator")
427                 << QStringLiteral("first_page")
428                 << QStringLiteral("last_page")
429                 << QStringLiteral("separator") // <title> in the part
430                 << QStringLiteral("action1")
431                 << QStringLiteral("action2")
432                 << QStringLiteral("undefined_group")
433                 << QStringLiteral("last_from_part")
434                 // End of <Merge>
435                 << QStringLiteral("host_after_merge")
436                 << QStringLiteral("host_after_merge_2")
437                 << QStringLiteral("separator")
438                 // Contents of <DefineGroup>
439                 << QStringLiteral("action_in_merge_group")
440                 // End of <DefineGroup>
441                 << QStringLiteral("separator") // <title> is a separator qaction with text
442                 << QStringLiteral("last_from_host")
443                 );
444         checkActions(fileMenu->actions(), QStringList()
445                 << QStringLiteral("file_new")
446                 << QStringLiteral("action_in_placed_merge")
447                 << QStringLiteral("file_open")
448                 << QStringLiteral("separator")
449                 << QStringLiteral("file_quit")
450                 << QStringLiteral("other_file_action"));
451         factory.removeClient(&partClient);
452         QCOMPARE(hostClient.domDocument().toString(), hostDomDoc);
453         // clang-format on
454     }
455     factory.removeClient(&hostClient);
456 }
457 
testPartMergingSettings()458 void KXmlGui_UnitTest::testPartMergingSettings() // #252911
459 {
460     const QByteArray hostXml =
461         "<?xml version = '1.0'?>\n"
462         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
463         "<gui version=\"1\" name=\"foo\" >\n"
464         "<MenuBar>\n"
465         // The solution was to remove the duplicated definition
466         //        " <Menu name=\"settings\"><text>&amp;Settings</text>\n"
467         //        "    <Action name=\"options_configure_keybinding\"/>\n"
468         //        "    <Action name=\"options_configure_toolbars\"/>\n"
469         //        "    <Merge name=\"configure_merge\"/>\n"
470         //        "    <Separator/>\n"
471         //        "    <Merge/>\n"
472         //        " </Menu>\n"
473         "</MenuBar></gui>\n";
474     TestGuiClient hostClient;
475     hostClient.createActions(QStringList() << QStringLiteral("options_configure_keybinding") << QStringLiteral("options_configure_toolbars"));
476     hostClient.createGUI(hostXml, true /*ui_standards.rc*/);
477     // qDebug() << hostClient.domDocument().toString();
478     KMainWindow mainWindow;
479     KXMLGUIBuilder builder(&mainWindow);
480     KXMLGUIFactory factory(&builder);
481     factory.addClient(&hostClient);
482     QWidget *settingsMenu = qobject_cast<QMenu *>(factory.container(QStringLiteral("settings"), &hostClient));
483     QVERIFY(settingsMenu);
484     // debugActions(settingsMenu->actions());
485 
486     const QByteArray partXml =
487         "<?xml version = '1.0'?>\n"
488         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
489         "<gui version=\"1\" name=\"foo\" >\n"
490         "<MenuBar>\n"
491         " <Menu name=\"settings\"><text>&amp;Settings</text>\n"
492         "    <Action name=\"configure_klinkstatus\"/>\n"
493         " </Menu>\n"
494         "</MenuBar></gui>\n";
495     TestGuiClient partClient(partXml);
496     partClient.createActions(QStringList() << QStringLiteral("configure_klinkstatus"));
497     factory.addClient(&partClient);
498     // debugActions(settingsMenu->actions());
499     checkActions(settingsMenu->actions(),
500                  QStringList() << QStringLiteral("separator") // that's ok, QMenuPrivate::filterActions won't show it
501                                << QStringLiteral("options_configure_keybinding") << QStringLiteral("options_configure_toolbars")
502                                << QStringLiteral("configure_klinkstatus"));
503     factory.removeClient(&partClient);
504     factory.removeClient(&hostClient);
505 }
506 
testUiStandardsMerging_data()507 void KXmlGui_UnitTest::testUiStandardsMerging_data()
508 {
509     QTest::addColumn<QByteArray>("xml");
510     QTest::addColumn<QStringList>("actions");
511     QTest::addColumn<QStringList>("expectedMenus");
512 
513     const QByteArray xmlBegin =
514         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
515         "<gui version=\"1\" name=\"foo\" >\n"
516         "<MenuBar>\n";
517     const QByteArray xmlEnd =
518         "</MenuBar>\n"
519         "</gui>";
520 
521     // Merging an empty menu (or a menu with only non-existing actions) would make
522     // the empty menu appear at the end after all other menus (fixed for KDE-4.2)
523     /* clang-format off */
524     QTest::newRow("empty file menu, implicit settings menu")
525         << QByteArray(xmlBegin + "<Menu name=\"file\"/>\n" + xmlEnd)
526         << (QStringList() << QStringLiteral("options_configure_toolbars"))
527         << (QStringList() << QStringLiteral("settings"));
528     QTest::newRow("file menu with non existing action, implicit settings menu")
529         << QByteArray(xmlBegin + "<Menu name=\"file\"><Action name=\"foo\"/></Menu>\n" + xmlEnd)
530         << (QStringList() << QStringLiteral("options_configure_toolbars"))
531         << (QStringList() << QStringLiteral("settings"));
532     QTest::newRow("file menu with existing action, implicit settings menu")
533         << QByteArray(xmlBegin + "<Menu name=\"file\"><Action name=\"open\"/></Menu>\n" + xmlEnd)
534         << (QStringList() << QStringLiteral("open") << QStringLiteral("options_configure_toolbars"))
535         << (QStringList() << QStringLiteral("file") << QStringLiteral("settings"));
536     QTest::newRow("implicit file and settings menu")
537         << QByteArray(xmlBegin + xmlEnd)
538         << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars"))
539         << (QStringList() << QStringLiteral("file") << QStringLiteral("settings")); // we could check that file_open is in the mainToolBar, too
540 
541     // Check that unknown non-empty menus are added at the "MergeLocal" position (before settings).
542     QTest::newRow("foo menu added at the end")
543         << QByteArray(xmlBegin + "<Menu name=\"foo\"><Action name=\"foo_action\"/></Menu>\n" + xmlEnd)
544         << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars") << QStringLiteral("foo_action"))
545         << (QStringList() << QStringLiteral("file") << QStringLiteral("foo") << QStringLiteral("settings"));
546 
547     QTest::newRow("Bille's testcase: menu patch + menu edit")
548         << QByteArray(xmlBegin + "<Menu name=\"patch\"><Action name=\"patch_generate\"/></Menu>\n"
549                       + "<Menu name=\"edit\"><Action name=\"edit_foo\"/></Menu>\n" + xmlEnd)
550         << (QStringList() << QStringLiteral("file_open") << QStringLiteral("patch_generate") << QStringLiteral("edit_foo"))
551         << (QStringList() << QStringLiteral("file") << QStringLiteral("edit") << QStringLiteral("patch"));
552     QTest::newRow("Bille's testcase: menu patch + menu edit, lowercase tag")
553         << QByteArray(xmlBegin + "<Menu name=\"patch\"><Action name=\"patch_generate\"/></Menu>\n"
554                       + "<menu name=\"edit\"><Action name=\"edit_foo\"/></menu>\n" + xmlEnd)
555         << (QStringList() << QStringLiteral("file_open") << QStringLiteral("patch_generate") << QStringLiteral("edit_foo"))
556         << (QStringList() << QStringLiteral("file") << QStringLiteral("edit") << QStringLiteral("patch"));
557 
558     // Check that <Menu append="..."> allows to insert menus at specific positions
559     QTest::newRow("Menu append")
560         << QByteArray(xmlBegin + "<Menu name=\"foo\" append=\"settings_merge\"><Action name=\"foo_action\"/></Menu>\n" + xmlEnd)
561         << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars")
562                           << QStringLiteral("foo_action") << QStringLiteral("help_contents"))
563         << (QStringList() << QStringLiteral("file") << QStringLiteral("settings") << QStringLiteral("foo") << QStringLiteral("help"));
564     QTest::newRow("Custom first menu")
565         << QByteArray(xmlBegin + "<Menu name=\"foo\" append=\"first_menu\"><Action name=\"foo_action\"/></Menu>\n" + xmlEnd)
566         << (QStringList() << QStringLiteral("edit_undo") << QStringLiteral("foo_action") << QStringLiteral("help_contents"))
567         << (QStringList() << QStringLiteral("foo") << QStringLiteral("edit") << QStringLiteral("help"));
568 
569     // Tests for noMerge="1"
570     QTest::newRow("noMerge empty file menu, implicit settings menu")
571         << QByteArray(xmlBegin + "<Menu name=\"file\" noMerge=\"1\"/>\n" + xmlEnd)
572         << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars"))
573         << (QStringList() << QStringLiteral("file") << QStringLiteral("settings")); // we keep empty menus, see #186382
574     QTest::newRow("noMerge empty file menu, file_open moved elsewhere")
575         << QByteArray(xmlBegin + "<Menu name=\"file\" noMerge=\"1\"/>\n<Menu name=\"foo\"><Action name=\"file_open\"/></Menu>" + xmlEnd)
576         << (QStringList() << QStringLiteral("file_open"))
577         << (QStringList() << QStringLiteral("file") << QStringLiteral("foo"));
578     QTest::newRow("noMerge file menu with open before new")
579         << QByteArray(xmlBegin + "<Menu name=\"file\" noMerge=\"1\"><Action name=\"file_open\"/><Action name=\"file_new\"/></Menu>" + xmlEnd)
580         << (QStringList() << QStringLiteral("file_open") << QStringLiteral("file_new"))
581         << (QStringList() << QStringLiteral("file")); // TODO check the order of the actions in the menu? how?
582 
583     // Tests for deleted="true"
584     QTest::newRow("deleted file menu, implicit settings menu")
585         << QByteArray(xmlBegin + "<Menu name=\"file\" deleted=\"true\"/>\n" + xmlEnd)
586         << (QStringList() << QStringLiteral("file_open") << QStringLiteral("options_configure_toolbars"))
587         << (QStringList() << QStringLiteral("settings"));
588     QTest::newRow("deleted file menu, file_open moved elsewhere")
589         << QByteArray(xmlBegin + "<Menu name=\"file\" deleted=\"true\"/>\n<Menu name=\"foo\"><Action name=\"file_open\"/></Menu>" + xmlEnd)
590         << (QStringList() << QStringLiteral("file_open"))
591         << (QStringList() << QStringLiteral("foo"));
592     QTest::newRow("deleted file menu with actions (contradiction)")
593         << QByteArray(xmlBegin + "<Menu name=\"file\" deleted=\"true\"><Action name=\"file_open\"/><Action name=\"file_new\"/></Menu>" + xmlEnd)
594         << (QStringList() << QStringLiteral("file_open") << QStringLiteral("file_new"))
595         << (QStringList());
596     /* clang-format on */
597 }
598 
testUiStandardsMerging()599 void KXmlGui_UnitTest::testUiStandardsMerging()
600 {
601     QFETCH(QByteArray, xml);
602     QFETCH(QStringList, actions);
603     QFETCH(QStringList, expectedMenus);
604 
605     TestGuiClient client;
606     client.createActions(actions);
607     client.createGUI(xml, true /*ui_standards.rc*/);
608 
609     const QDomDocument domDocument = client.domDocument();
610     const QDomElement docElem = domDocument.documentElement();
611     QCOMPARE(docElem.attribute(QStringLiteral("name")), QStringLiteral("foo")); // not standard_containers from ui_standards.rc
612     QCOMPARE(docElem.attribute(QStringLiteral("version")), QStringLiteral("1")); // not standard_containers from ui_standards.rc
613 
614     KMainWindow mainWindow;
615     KXMLGUIBuilder builder(&mainWindow);
616     KXMLGUIFactory factory(&builder);
617     factory.addClient(&client);
618 
619     const QStringList containerNames = collectMenuNames(factory);
620     // qDebug() << containerNames;
621     QCOMPARE(containerNames, expectedMenus);
622 
623     factory.removeClient(&client);
624 }
625 
testActionListAndSeparator()626 void KXmlGui_UnitTest::testActionListAndSeparator()
627 {
628     const QByteArray xml =
629         "<?xml version = '1.0'?>\n"
630         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
631         "<gui version=\"1\" name=\"foo\" >\n"
632         "<MenuBar>\n"
633         " <Menu name=\"groups\"><text>Add to Group</text>\n"
634         "  <ActionList name=\"view_groups_list\"/>\n"
635         "  <Separator />"
636         "  <Action name=\"view_add_to_new_group\" />\n"
637         "  <ActionList name=\"second_list\"/>\n"
638         " </Menu>\n"
639         "</MenuBar>\n"
640         "</gui>";
641 
642     TestGuiClient client(xml);
643     client.createActions(QStringList() << QStringLiteral("view_add_to_new_group"));
644     KMainWindow mainWindow;
645     KXMLGUIBuilder builder(&mainWindow);
646     KXMLGUIFactory factory(&builder);
647     factory.addClient(&client);
648 
649     QWidget *menuW = factory.container(QStringLiteral("groups"), &client);
650     QVERIFY(menuW);
651     QMenu *menu = qobject_cast<QMenu *>(menuW);
652     QVERIFY(menu);
653 
654     // debugActions(menu->actions());
655     checkActions(menu->actions(),
656                  QStringList() << QStringLiteral("separator") // that's ok, QMenuPrivate::filterActions won't show it
657                                << QStringLiteral("view_add_to_new_group"));
658 
659     qDebug() << "Now plugging the actionlist";
660 
661     QAction *action1 = new QAction(this);
662     action1->setObjectName(QStringLiteral("action1"));
663     client.actionCollection()->setDefaultShortcut(action1, QKeySequence(QStringLiteral("Ctrl+2")));
664     QAction *action2 = new QAction(this);
665     action2->setObjectName(QStringLiteral("action2"));
666     const QList<QAction *> actionList = {action1, action2};
667     client.plugActionList(QStringLiteral("view_groups_list"), actionList);
668     QCOMPARE(QKeySequence::listToString(action1->shortcuts()), QStringLiteral("Ctrl+2"));
669 
670     const QStringList expectedActionsOneList = {QStringLiteral("action1"),
671                                                 QStringLiteral("action2"),
672                                                 QStringLiteral("separator"),
673                                                 QStringLiteral("view_add_to_new_group")};
674     // debugActions(menu->actions());
675     checkActions(menu->actions(), expectedActionsOneList);
676 
677     QAction *action3 = new QAction(this);
678     action3->setObjectName(QStringLiteral("action3"));
679     const QList<QAction *> secondActionList = {action3};
680     client.plugActionList(QStringLiteral("second_list"), secondActionList);
681     QStringList expectedActions = expectedActionsOneList;
682     expectedActions << QStringLiteral("action3");
683     checkActions(menu->actions(), expectedActions);
684 
685     qDebug() << "Now remove+add gui client";
686 
687     // While I'm here, what happens with the action list if I remove+add the guiclient,
688     // like KXmlGuiWindow::newToolBarConfig does?
689     factory.removeClient(&client);
690     factory.addClient(&client);
691     // We need to get the container widget again, it was re-created.
692     menuW = factory.container(QStringLiteral("groups"), &client);
693     QVERIFY(menuW);
694     menu = qobject_cast<QMenu *>(menuW);
695     // debugActions(menu->actions());
696     checkActions(menu->actions(),
697                  QStringList() << QStringLiteral("separator") // yep, it removed the actionlist thing...
698                                << QStringLiteral("view_add_to_new_group"));
699     qDebug() << "Now plugging the actionlist again";
700     client.plugActionList(QStringLiteral("second_list"), secondActionList);
701     client.plugActionList(QStringLiteral("view_groups_list"), actionList);
702     checkActions(menu->actions(), expectedActions);
703     factory.removeClient(&client);
704 }
705 
testHiddenToolBar()706 void KXmlGui_UnitTest::testHiddenToolBar()
707 {
708     const QByteArray xml =
709         "<?xml version = '1.0'?>\n"
710         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
711         "<gui version=\"1\" name=\"foo\" >\n"
712         "<MenuBar>\n"
713         "</MenuBar>\n"
714         "<ToolBar hidden=\"true\" name=\"mainToolBar\">\n"
715         "  <Action name=\"go_up\"/>\n"
716         "</ToolBar>\n"
717         "<ToolBar name=\"visibleToolBar\">\n"
718         "  <Action name=\"go_up\"/>\n"
719         "</ToolBar>\n"
720         "<ToolBar hidden=\"true\" name=\"hiddenToolBar\">\n"
721         "  <Action name=\"go_up\"/>\n"
722         "</ToolBar>\n"
723         "</gui>\n";
724     KConfigGroup cg(KSharedConfig::openConfig(), "testHiddenToolBar");
725     TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc");
726     mainWindow.setAutoSaveSettings(cg);
727     mainWindow.createActions(QStringList() << QStringLiteral("go_up"));
728     mainWindow.createGUI();
729 
730     KToolBar *mainToolBar = mainWindow.toolBarByName(QStringLiteral("mainToolBar"));
731     QVERIFY(mainToolBar->isHidden());
732 
733     KXMLGUIFactory *factory = mainWindow.guiFactory();
734     QVERIFY(!factory->container(QStringLiteral("visibleToolBar"), &mainWindow)->isHidden());
735     KToolBar *hiddenToolBar = qobject_cast<KToolBar *>(factory->container(QStringLiteral("hiddenToolBar"), &mainWindow));
736     qDebug() << hiddenToolBar;
737     QVERIFY(hiddenToolBar->isHidden());
738 
739     // Now open KEditToolBar (#105525)
740     KEditToolBar editToolBar(factory);
741     // KEditToolBar loads the stuff in showEvent...
742     QShowEvent ev;
743     qApp->sendEvent(&editToolBar, &ev);
744     clickApply(&editToolBar);
745     QVERIFY(qobject_cast<KToolBar *>(factory->container(QStringLiteral("hiddenToolBar"), &mainWindow))->isHidden());
746 
747     mainWindow.close();
748 }
749 
750 // taken from KMainWindow_UnitTest::testAutoSaveSettings()
testAutoSaveSettings()751 void KXmlGui_UnitTest::testAutoSaveSettings()
752 {
753     const QByteArray xml =
754         "<?xml version = '1.0'?>\n"
755         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
756         "<gui version=\"1\" name=\"foo\" >\n"
757         "<MenuBar>\n"
758         "</MenuBar>\n"
759         "<ToolBar name=\"mainToolBar\">\n"
760         "  <Action name=\"go_up\"/>\n"
761         "</ToolBar>\n"
762         "<ToolBar name=\"secondToolBar\">\n"
763         "  <Action name=\"go_up\"/>\n"
764         "</ToolBar>\n"
765         "</gui>\n";
766     {
767         // do not interfere with the "toolbarVisibility" unit test
768         KConfigGroup cg(KSharedConfig::openConfig(), "testAutoSaveSettings");
769         TestXmlGuiWindow mw(xml, "kxmlgui_unittest.rc");
770         mw.show();
771         mw.setAutoSaveSettings(cg);
772 
773         // Test resizing first (like show() does).
774         mw.reallyResize(400, 400);
775         QTest::qWait(200);
776 
777         mw.createActions(QStringList() << QStringLiteral("go_up"));
778         mw.createGUI();
779 
780         // Resize again, should be saved
781         mw.reallyResize(800, 600);
782         QTest::qWait(200);
783 
784         KToolBar *mainToolBar = mw.toolBarByName(QStringLiteral("mainToolBar"));
785         QCOMPARE(mw.toolBarArea(mainToolBar), Qt::TopToolBarArea);
786         KToolBar *secondToolBar = mw.toolBarByName(QStringLiteral("secondToolBar"));
787         QCOMPARE((int)mw.toolBarArea(secondToolBar), (int)Qt::TopToolBarArea); // REFERENCE #1 (see below)
788 
789         // Move second toolbar to bottom
790         const QPoint oldPos = secondToolBar->pos();
791         mw.addToolBar(Qt::BottomToolBarArea, secondToolBar);
792         const QPoint newPos = secondToolBar->pos();
793         QCOMPARE(mw.toolBarArea(secondToolBar), Qt::BottomToolBarArea);
794         // Calling to addToolBar is not enough to trigger the event filter for move events
795         // in KMainWindow, because there is no layouting happening in hidden mainwindows.
796         QMoveEvent moveEvent(newPos, oldPos);
797         QApplication::sendEvent(secondToolBar, &moveEvent);
798 
799         mw.close();
800     }
801 
802     {
803         KConfigGroup cg(KSharedConfig::openConfig(), "testAutoSaveSettings");
804         TestXmlGuiWindow mw2(xml, "kxmlgui_unittest.rc");
805         mw2.show();
806         mw2.setAutoSaveSettings(cg);
807         QTest::qWait(200);
808         // Check window size was restored
809         QCOMPARE(mw2.size(), QSize(800, 600));
810 
811         mw2.createActions(QStringList() << QStringLiteral("go_up"));
812         mw2.createGUI();
813 
814         // Force window layout to happen
815         mw2.reallyResize(800, 600);
816         QTest::qWait(200);
817 
818         // Check toolbar positions were restored
819         KToolBar *mainToolBar = mw2.toolBarByName(QStringLiteral("mainToolBar"));
820         QCOMPARE(mw2.toolBarArea(mainToolBar), Qt::TopToolBarArea);
821         KToolBar *secondToolBar = mw2.toolBarByName(QStringLiteral("secondToolBar"));
822         QCOMPARE(mw2.toolBarArea(secondToolBar), Qt::BottomToolBarArea);
823         mw2.applyMainWindowSettings(mw2.autoSaveConfigGroup());
824         QCOMPARE(mw2.toolBarArea(secondToolBar), Qt::BottomToolBarArea);
825     }
826 }
827 
testDeletedContainers()828 void KXmlGui_UnitTest::testDeletedContainers() // deleted="true"
829 {
830     const QByteArray xml =
831         "<?xml version = '1.0'?>\n"
832         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
833         "<gui version=\"1\" name=\"foo\" >\n"
834         "<MenuBar>\n"
835         "  <Menu deleted=\"true\" name=\"game\"/>\n"
836         "</MenuBar>\n"
837         "<ToolBar deleted=\"true\" name=\"mainToolBar\">\n"
838         "  <Action name=\"go_up\"/>\n"
839         "</ToolBar>\n"
840         "<ToolBar name=\"visibleToolBar\">\n"
841         "  <Action name=\"go_up\"/>\n"
842         "</ToolBar>\n"
843         "<ToolBar deleted=\"true\" name=\"deletedToolBar\">\n"
844         "  <Action name=\"go_up\"/>\n"
845         "</ToolBar>\n"
846         "</gui>\n";
847     KConfigGroup cg(KSharedConfig::openConfig(), "testDeletedToolBar");
848     TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc");
849     mainWindow.setAutoSaveSettings(cg);
850     mainWindow.createActions(QStringList() << QStringLiteral("go_up") << QStringLiteral("file_new") << QStringLiteral("game_new"));
851     mainWindow.createGUI();
852     KXMLGUIFactory *factory = mainWindow.guiFactory();
853 
854     // qDebug() << "containers:" << factory->containers("ToolBar");
855     QVERIFY(!factory->container(QStringLiteral("mainToolBar"), &mainWindow));
856     QVERIFY(!factory->container(QStringLiteral("visibleToolBar"), &mainWindow)->isHidden());
857     QVERIFY(!factory->container(QStringLiteral("deletedToolBar"), &mainWindow));
858     QVERIFY(factory->container(QStringLiteral("file"), &mainWindow)); // File menu was created
859     QVERIFY(!factory->container(QStringLiteral("game"), &mainWindow)); // Game menu was not created
860 
861     // Now open KEditToolBar, just to check it doesn't crash.
862     KEditToolBar editToolBar(factory);
863     // KEditToolBar loads the stuff in showEvent...
864     QShowEvent ev;
865     qApp->sendEvent(&editToolBar, &ev);
866     clickApply(&editToolBar);
867     QVERIFY(!factory->container(QStringLiteral("mainToolBar"), &mainWindow));
868     QVERIFY(!factory->container(QStringLiteral("visibleToolBar"), &mainWindow)->isHidden());
869     QVERIFY(!factory->container(QStringLiteral("deletedToolBar"), &mainWindow));
870     QVERIFY(factory->container(QStringLiteral("file"), &mainWindow));
871     QVERIFY(!factory->container(QStringLiteral("game"), &mainWindow));
872 
873     mainWindow.close();
874 }
875 
testTopLevelSeparator()876 void KXmlGui_UnitTest::testTopLevelSeparator()
877 {
878     const QByteArray xml =
879         "<?xml version = '1.0'?>\n"
880         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
881         "<gui version=\"1\" name=\"foo\" >\n"
882         "<MenuBar>\n"
883         " <Menu name=\"before_separator\"><text>Before Separator</text></Menu>\n"
884         " <Separator />\n"
885         " <Menu name=\"after_separator\"><text>After Separator</text></Menu>\n"
886         "</MenuBar>\n"
887         "</gui>";
888 
889     TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc");
890     mainWindow.createGUI();
891 
892     checkActions(mainWindow.menuBar()->actions(),
893                  QStringList() << QStringLiteral("before_separator") << QStringLiteral("separator") << QStringLiteral("after_separator")
894                                << QStringLiteral("settings") << QStringLiteral("separator") << QStringLiteral("help"));
895 }
896 
897 // Check that the objectName() of the menus is set from the name in the XML file
testMenuNames()898 void KXmlGui_UnitTest::testMenuNames()
899 {
900     const QByteArray xml =
901         "<?xml version = '1.0'?>\n"
902         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
903         "<gui version=\"1\" name=\"foo\" >\n"
904         "<MenuBar>\n"
905         " <Menu name=\"filemenu\"><text>File Menu</text></Menu>\n"
906         "</MenuBar>\n"
907         "</gui>";
908 
909     TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc");
910     mainWindow.createGUI();
911 
912     checkActions(mainWindow.menuBar()->actions(),
913                  QStringList() << QStringLiteral("filemenu") << QStringLiteral("settings") << QStringLiteral("separator") << QStringLiteral("help"));
914 }
915 
916 // Test what happens when the application's rc file isn't found
917 // We want a warning to be printed, but we don't want to see all menus from ui_standards.rc
testMenusNoXmlFile()918 void KXmlGui_UnitTest::testMenusNoXmlFile()
919 {
920     TestXmlGuiWindow mainWindow(QByteArray(), "nolocalfile_either.rc");
921     mainWindow.createGUIBad();
922 
923     checkActions(mainWindow.menuBar()->actions(), QStringList{QStringLiteral("settings"), QStringLiteral("separator"), QStringLiteral("help")});
924 }
925 
testXMLFileReplacement()926 void KXmlGui_UnitTest::testXMLFileReplacement()
927 {
928     // to differentiate "original" and replacement xml file, one is created with "modified" toolbars
929     QTemporaryFile fileOrig;
930     QVERIFY(fileOrig.open());
931     createXmlFile(fileOrig, 2, AddToolBars);
932     const QString filenameOrig = fileOrig.fileName();
933     fileOrig.close();
934 
935     QTemporaryFile fileReplace;
936     QVERIFY(fileReplace.open());
937     createXmlFile(fileReplace, 2, AddModifiedToolBars);
938     const QString filenameReplace = fileReplace.fileName();
939     fileReplace.close();
940 
941     // finally, our local xml file has <ActionProperties/>
942     const QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kxmlgui_unittest");
943     QFile fileLocal(dir + QLatin1String("/testui.rc"));
944     QVERIFY2(fileLocal.open(QIODevice::WriteOnly), qPrintable(fileLocal.fileName()));
945     createXmlFile(fileLocal, 1, AddActionProperties);
946     const QString filenameLocal = fileLocal.fileName();
947     fileLocal.close();
948 
949     TestGuiClient client;
950     // first make sure that the "original" file is loaded, correctly
951     client.setXMLFilePublic(filenameOrig);
952     QString xml = client.domDocument().toString();
953     // qDebug() << xml;
954     QVERIFY(xml.contains(QLatin1String("<Action name=\"print\"")));
955     QVERIFY(!xml.contains(QLatin1String("<Action name=\"home\"")));
956     QVERIFY(!xml.contains(QLatin1String("<ActionProperties>")));
957 
958     // now test the replacement (+ local file)
959     client.replaceXMLFile(filenameReplace, filenameLocal);
960     xml = client.domDocument().toString();
961     QVERIFY(!xml.contains(QLatin1String("<Action name=\"print\"")));
962     QVERIFY(xml.contains(QLatin1String("<Action name=\"home\"")));
963     QVERIFY(xml.contains(QLatin1String("<ActionProperties>")));
964 
965     // re-check after a reload
966     client.reloadXML();
967     QString reloadedXml = client.domDocument().toString();
968     QVERIFY(!reloadedXml.contains(QLatin1String("<Action name=\"print\"")));
969     QVERIFY(reloadedXml.contains(QLatin1String("<Action name=\"home\"")));
970     QVERIFY(reloadedXml.contains(QLatin1String("<ActionProperties>")));
971 
972     // Check what happens when the local file doesn't exist
973     TestGuiClient client2;
974     QFile::remove(filenameLocal);
975     client2.replaceXMLFile(filenameReplace, filenameLocal);
976     xml = client2.domDocument().toString();
977     // qDebug() << xml;
978     QVERIFY(!xml.contains(QLatin1String("<Action name=\"print\"")));
979     QVERIFY(xml.contains(QLatin1String("<Action name=\"home\""))); // modified toolbars
980     QVERIFY(!xml.contains(QLatin1String("<ActionProperties>"))); // but no local xml file
981 }
982 
testClientDestruction()983 void KXmlGui_UnitTest::testClientDestruction() // #170806
984 {
985     const QByteArray hostXml =
986         "<?xml version = '1.0'?>\n"
987         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
988         "<gui version=\"1\" name=\"foo\" >\n"
989         "<MenuBar>\n"
990         " <Menu name=\"file\"><text>&amp;File</text>\n"
991         " </Menu>\n"
992         " <Merge/>\n"
993         "</MenuBar>\n"
994         "</gui>";
995     const QByteArray xml =
996         "<?xml version = '1.0'?>\n"
997         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
998         "<gui version=\"1\" name=\"foo\" >\n"
999         "<MenuBar>\n"
1000         " <Menu name=\"file\"><text>&amp;File</text>\n"
1001         "  <Action name=\"file_open\"/>\n"
1002         "  <Action name=\"file_quit\"/>\n"
1003         " </Menu>\n"
1004         "</MenuBar>\n"
1005         "</gui>";
1006 
1007     TestXmlGuiWindow mainWindow(hostXml, "kxmlgui_unittest.rc");
1008     TestGuiClient *client = new TestGuiClient(xml);
1009     client->createActions(QStringList() << QStringLiteral("file_open") << QStringLiteral("file_quit"));
1010     mainWindow.insertChildClient(client);
1011     mainWindow.createGUI();
1012 
1013     const QStringList menus = {QStringLiteral("file"), QStringLiteral("settings"), QStringLiteral("separator"), QStringLiteral("help")};
1014     checkActions(mainWindow.menuBar()->actions(), menus);
1015 
1016     QVERIFY(mainWindow.factory()->clients().contains(client));
1017     delete client;
1018     QVERIFY(!mainWindow.factory()->clients().contains(client));
1019 
1020     // No change, because deletion is fast, it doesn't do manual unplugging.
1021     checkActions(mainWindow.menuBar()->actions(), menus);
1022 }
1023 
testShortcuts()1024 void KXmlGui_UnitTest::testShortcuts()
1025 {
1026     const QByteArray xml =
1027         "<?xml version = '1.0'?>\n"
1028         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
1029         "<gui version=\"1\" name=\"foo\" >\n"
1030         "<MenuBar>\n"
1031         " <Menu name=\"file\"><text>&amp;File</text>\n"
1032         "  <Action name=\"file_open\" shortcut=\"Ctrl+O\"/>\n"
1033         "  <Action name=\"file_quit\" shortcut=\"Ctrl+Q; Ctrl+D\"/>\n"
1034         " </Menu>\n"
1035         "</MenuBar>\n"
1036         "<ActionProperties scheme=\"Default\">\n"
1037         "  <Action shortcut=\"Ctrl+O\" name=\"file_open\"/>\n"
1038         "  <Action shortcut=\"Ctrl+Q; Ctrl+D\" name=\"file_quit\"/>\n"
1039         "</ActionProperties>\n"
1040         "</gui>";
1041 
1042     TestGuiClient client;
1043     client.createActions(QStringList() << QStringLiteral("file_open") << QStringLiteral("file_quit"));
1044     client.createGUI(xml, false /*ui_standards.rc*/);
1045 
1046     KMainWindow mainWindow;
1047     KXMLGUIBuilder builder(&mainWindow);
1048     KXMLGUIFactory factory(&builder);
1049     factory.addClient(&client);
1050 
1051     QAction *actionOpen = client.action("file_open");
1052     QAction *actionQuit = client.action("file_quit");
1053     QVERIFY(actionOpen && actionQuit);
1054     QCOMPARE(actionOpen->shortcuts(), QList<QKeySequence>() << QKeySequence(QStringLiteral("Ctrl+O")));
1055     // #345411
1056     QCOMPARE(actionQuit->shortcuts(), QList<QKeySequence>() << QKeySequence(QStringLiteral("Ctrl+Q")) << QKeySequence(QStringLiteral("Ctrl+D")));
1057 
1058     factory.removeClient(&client);
1059 }
1060 
testPopupMenuParent()1061 void KXmlGui_UnitTest::testPopupMenuParent()
1062 {
1063     const QByteArray xml =
1064         "<?xml version = '1.0'?>\n"
1065         "<!DOCTYPE gui SYSTEM \"kpartgui.dtd\">\n"
1066         "<gui version=\"1\" name=\"foo\" >\n"
1067         "<Menu name=\"foo\"><text>Foo</text></Menu>\n"
1068         "</gui>";
1069 
1070     TestXmlGuiWindow mainWindow(xml, "kxmlgui_unittest.rc");
1071     mainWindow.createGUI();
1072 
1073     auto popupMenu = mainWindow.menuByName(QStringLiteral("foo"));
1074     QVERIFY(popupMenu);
1075     QCOMPARE(popupMenu->parent(), &mainWindow);
1076 }
1077 
testSpecificApplicationLanguageQLocale()1078 void KXmlGui_UnitTest::testSpecificApplicationLanguageQLocale()
1079 {
1080     qunsetenv("LC_ALL"); // In case the user sets the LC_ALL ENV variable this test could fail
1081     const QLocale originalSystemLocale = QLocale::system();
1082 
1083     KDEPrivate::setApplicationSpecificLanguage("ru_RU");
1084     KDEPrivate::initializeLanguages();
1085 
1086 #ifdef Q_OS_WIN
1087     QEXPECT_FAIL("",
1088                  "KDEPrivate::initializeLanguages assumes that setting $LANGUAGE will have an effect, this isn't the case on Windows. Needs to be fixed.",
1089                  Abort);
1090 #endif
1091     QCOMPARE(QLocale::system().language(), QLocale::Russian);
1092 
1093     KDEPrivate::setApplicationSpecificLanguage("wa");
1094     KDEPrivate::initializeLanguages();
1095 
1096     QCOMPARE(QLocale::system().language(), QLocale::Walloon);
1097 
1098     KDEPrivate::setApplicationSpecificLanguage(QByteArray());
1099     KDEPrivate::initializeLanguages();
1100 
1101     QCOMPARE(QLocale::system(), originalSystemLocale);
1102 }
1103 
testSingleModifierQKeySequenceEndsWithPlus()1104 void KXmlGui_UnitTest::testSingleModifierQKeySequenceEndsWithPlus()
1105 {
1106     // Check that native texts of the Meta, Alt, Control, Shift, Keypad modifiers end in "+"
1107     // we depend on that in KKeySequenceWidgetPrivate::updateShortcutDisplay()
1108     QVERIFY(QKeySequence(Qt::MetaModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+')));
1109     QVERIFY(QKeySequence(Qt::AltModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+')));
1110     QVERIFY(QKeySequence(Qt::ControlModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+')));
1111     QVERIFY(QKeySequence(Qt::ShiftModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+')));
1112     QVERIFY(QKeySequence(Qt::KeypadModifier).toString(QKeySequence::NativeText).endsWith(QLatin1Char('+')));
1113 }
1114 
testSaveShortcutsAndRefresh()1115 void KXmlGui_UnitTest::testSaveShortcutsAndRefresh()
1116 {
1117     QTemporaryFile xmlFile;
1118     QVERIFY(xmlFile.open());
1119     createXmlFile(xmlFile, 2, AddModifiedMenus);
1120     const QString filename = xmlFile.fileName();
1121     xmlFile.close();
1122 
1123     QTemporaryFile localXmlFile;
1124     QVERIFY(localXmlFile.open());
1125     const QString localFilename = localXmlFile.fileName();
1126     localXmlFile.close();
1127 
1128     TestGuiClient client;
1129     client.createActions({QStringLiteral("file_open")});
1130     client.setLocalXMLFilePublic(localFilename);
1131     client.setXMLFilePublic(filename);
1132 
1133     QWidget w;
1134     KXMLGUIBuilder builder(&w);
1135     KXMLGUIFactory factory(&builder);
1136     factory.addClient(&client);
1137     factory.removeClient(&client);
1138     factory.addClient(&client);
1139 
1140     QAction *a = client.actionCollection()->action(QStringLiteral("file_open"));
1141     QCOMPARE(a->shortcut(), QKeySequence());
1142     a->setShortcut(Qt::Key_F22);
1143     QCOMPARE(a->shortcut(), QKeySequence(Qt::Key_F22));
1144 
1145     client.actionCollection()->writeSettings();
1146 
1147     factory.refreshActionProperties();
1148 
1149     a = client.actionCollection()->action(QStringLiteral("file_open"));
1150     QCOMPARE(a->shortcut(), QKeySequence(Qt::Key_F22));
1151 }
1152