1/*
2 * Copyright (C) 2012 Igalia S.L.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27
28#include "WTFStringUtilities.h"
29#include <WebKit/InputMethodFilter.h>
30#include <gdk/gdkkeysyms.h>
31#include <gtk/gtk.h>
32#include <wtf/glib/GRefPtr.h>
33#include <wtf/glib/GUniquePtr.h>
34#include <wtf/text/CString.h>
35
36using namespace WebKit;
37
38namespace TestWebKitAPI {
39
40class TestInputMethodFilter : public InputMethodFilter {
41public:
42 TestInputMethodFilter()
43 : m_testWindow(gtk_window_new(GTK_WINDOW_POPUP))
44 {
45 setTestingMode(true);
46
47 gtk_widget_show(m_testWindow);
48 gtk_im_context_set_client_window(context(), gtk_widget_get_window(m_testWindow));
49
50 setEnabled(true);
51 }
52
53 ~TestInputMethodFilter()
54 {
55 gtk_widget_destroy(m_testWindow);
56 }
57
58 void sendKeyEventToFilter(unsigned gdkKeyValue, GdkEventType type, unsigned modifiers = 0)
59 {
60 GdkEvent* event = gdk_event_new(type);
61 event->key.keyval = gdkKeyValue;
62 event->key.state = modifiers;
63 event->key.window = gtk_widget_get_window(m_testWindow);
64 event->key.time = GDK_CURRENT_TIME;
65 g_object_ref(event->key.window);
66 gdk_event_set_device(event, gdk_device_manager_get_client_pointer(gdk_display_get_device_manager(gdk_display_get_default())));
67
68 GUniqueOutPtr<GdkKeymapKey> keys;
69 gint nKeys;
70 if (gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(), gdkKeyValue, &keys.outPtr(), &nKeys) && nKeys)
71 event->key.hardware_keycode = keys.get()[0].keycode;
72
73 filterKeyEvent(&event->key);
74 gdk_event_free(event);
75 }
76
77 void sendPressAndReleaseKeyEventPairToFilter(unsigned gdkKeyValue, unsigned modifiers = 0)
78 {
79 sendKeyEventToFilter(gdkKeyValue, GDK_KEY_PRESS, modifiers);
80 sendKeyEventToFilter(gdkKeyValue, GDK_KEY_RELEASE, modifiers);
81 }
82
83private:
84 GtkWidget* m_testWindow;
85};
86
87TEST(WebKit2, InputMethodFilterSimple)
88{
89 TestInputMethodFilter inputMethodFilter;
90 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_g);
91 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_t);
92 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_k);
93
94 const Vector<String>& events = inputMethodFilter.events();
95
96 ASSERT_EQ(6, events.size());
97 ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=67 text='g'"), events[0]);
98 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=67"), events[1]);
99 ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=74 text='t'"), events[2]);
100 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=74"), events[3]);
101 ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=6B text='k'"), events[4]);
102 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=6B"), events[5]);
103}
104
105TEST(WebKit2, InputMethodFilterUnicodeSequence)
106{
107 TestInputMethodFilter inputMethodFilter;
108
109 // This is simple unicode hex entry of the characters, u, 0, 0, f, 4 pressed with
110 // the shift and controls keys held down. In reality, these values are not typical
111 // of an actual hex entry, because they'd be transformed by the shift modifier according
112 // to the keyboard layout. For instance, on a US keyboard a 0 with the shift key pressed
113 // is a right parenthesis. Using these values prevents having to work out what the
114 // transformed characters are based on the current keyboard layout.
115 inputMethodFilter.sendKeyEventToFilter(GDK_KEY_Control_L, GDK_KEY_PRESS);
116 inputMethodFilter.sendKeyEventToFilter(GDK_KEY_Shift_L, GDK_KEY_PRESS, GDK_CONTROL_MASK);
117
118 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_U, GDK_SHIFT_MASK | GDK_CONTROL_MASK);
119 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_0, GDK_SHIFT_MASK | GDK_CONTROL_MASK);
120 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_0, GDK_SHIFT_MASK | GDK_CONTROL_MASK);
121 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_F, GDK_SHIFT_MASK | GDK_CONTROL_MASK);
122 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_4, GDK_SHIFT_MASK | GDK_CONTROL_MASK);
123
124 inputMethodFilter.sendKeyEventToFilter(GDK_KEY_Shift_L, GDK_KEY_RELEASE, GDK_CONTROL_MASK | GDK_SHIFT_MASK);
125 inputMethodFilter.sendKeyEventToFilter(GDK_KEY_Control_L, GDK_KEY_RELEASE, GDK_CONTROL_MASK);
126
127 const Vector<String>& events = inputMethodFilter.events();
128 ASSERT_EQ(21, events.size());
129 ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=FFE3"), events[0]);
130 ASSERT_EQ(String("sendSimpleKeyEvent type=press keycode=FFE1"), events[1]);
131 ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=55"), events[2]);
132 ASSERT_EQ(String("setPreedit text='u' cursorOffset=1"), events[3]);
133 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=55"), events[4]);
134 ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=30"), events[5]);
135 ASSERT_EQ(String("setPreedit text='u0' cursorOffset=2"), events[6]);
136 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=30"), events[7]);
137 ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=30"), events[8]);
138 ASSERT_EQ(String("setPreedit text='u00' cursorOffset=3"), events[9]);
139 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=30"), events[10]);
140 ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=46"), events[11]);
141 ASSERT_EQ(String("setPreedit text='u00F' cursorOffset=4"), events[12]);
142 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=46"), events[13]);
143 ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=34"), events[14]);
144 ASSERT_EQ(String("setPreedit text='u00F4' cursorOffset=5"), events[15]);
145 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=34"), events[16]);
146 ASSERT_EQ(String::fromUTF8("confirmComposition 'ô'"), events[17]);
147 ASSERT_EQ(String("setPreedit text='' cursorOffset=0"), events[18]);
148 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=FFE1"), events[19]);
149 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=FFE3"), events[20]);
150}
151
152TEST(WebKit2, InputMethodFilterComposeKey)
153{
154 TestInputMethodFilter inputMethodFilter;
155
156 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_Multi_key);
157 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_apostrophe);
158 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_o);
159
160 const Vector<String>& events = inputMethodFilter.events();
161 ASSERT_EQ(3, events.size());
162 ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=6F"), events[0]);
163 ASSERT_EQ(String::fromUTF8("confirmComposition 'ó'"), events[1]);
164 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=6F"), events[2]);
165}
166
167typedef void (*GetPreeditStringCallback) (GtkIMContext*, gchar**, PangoAttrList**, int*);
168static void temporaryGetPreeditStringOverride(GtkIMContext*, char** string, PangoAttrList** attrs, int* cursorPosition)
169{
170 *string = g_strdup("preedit of doom, bringer of cheese");
171 *cursorPosition = 3;
172}
173
174TEST(WebKit2, InputMethodFilterContextEventsWithoutKeyEvents)
175{
176 TestInputMethodFilter inputMethodFilter;
177
178 // This is a bit of a hack to avoid mocking out the entire InputMethodContext, by
179 // simply replacing the get_preedit_string virtual method for the length of this test.
180 GtkIMContext* context = inputMethodFilter.context();
181 GtkIMContextClass* contextClass = GTK_IM_CONTEXT_GET_CLASS(context);
182 GetPreeditStringCallback previousCallback = contextClass->get_preedit_string;
183 contextClass->get_preedit_string = temporaryGetPreeditStringOverride;
184
185 g_signal_emit_by_name(context, "preedit-changed");
186 g_signal_emit_by_name(context, "commit", "commit text");
187
188 contextClass->get_preedit_string = previousCallback;
189
190 const Vector<String>& events = inputMethodFilter.events();
191 ASSERT_EQ(6, events.size());
192 ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=FFFFFF (faked)"), events[0]);
193 ASSERT_EQ(String("setPreedit text='preedit of doom, bringer of cheese' cursorOffset=3"), events[1]);
194 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=FFFFFF (faked)"), events[2]);
195 ASSERT_EQ(String("sendKeyEventWithCompositionResults type=press keycode=FFFFFF (faked)"), events[3]);
196 ASSERT_EQ(String("confirmComposition 'commit text'"), events[4]);
197 ASSERT_EQ(String("sendSimpleKeyEvent type=release keycode=FFFFFF (faked)"), events[5]);
198}
199
200static bool gSawContextReset = false;
201typedef void (*ResetCallback) (GtkIMContext*);
202static void temporaryResetOverride(GtkIMContext*)
203{
204 gSawContextReset = true;
205}
206
207TEST(WebKit2, InputMethodFilterContextFocusOutDuringOngoingComposition)
208{
209 TestInputMethodFilter inputMethodFilter;
210
211 // See comment above about this technique.
212 GtkIMContext* context = inputMethodFilter.context();
213 GtkIMContextClass* contextClass = GTK_IM_CONTEXT_GET_CLASS(context);
214 ResetCallback previousCallback = contextClass->reset;
215 contextClass->reset = temporaryResetOverride;
216
217 gSawContextReset = false;
218 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_Multi_key);
219 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_apostrophe);
220 inputMethodFilter.notifyFocusedOut();
221 ASSERT_TRUE(gSawContextReset);
222
223 contextClass->reset = previousCallback;
224}
225
226TEST(WebKit2, InputMethodFilterContextMouseClickDuringOngoingComposition)
227{
228 TestInputMethodFilter inputMethodFilter;
229
230 // See comment above about this technique.
231 GtkIMContext* context = inputMethodFilter.context();
232 GtkIMContextClass* contextClass = GTK_IM_CONTEXT_GET_CLASS(context);
233 ResetCallback previousCallback = contextClass->reset;
234 contextClass->reset = temporaryResetOverride;
235
236 gSawContextReset = false;
237 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_Multi_key);
238 inputMethodFilter.sendPressAndReleaseKeyEventPairToFilter(GDK_KEY_apostrophe);
239 inputMethodFilter.notifyMouseButtonPress();
240 ASSERT_TRUE(gSawContextReset);
241
242 contextClass->reset = previousCallback;
243}
244
245} // namespace TestWebKitAPI
246